diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ecf0c46..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "go.toolsEnvVars": { - "GOOS": "linux", - "GOARCH": "arm", - "GOROOT": "/home/pato/.cache/tinygo/goroot-66ab96b98619019787146f7e3961483ad5d2960665e9959cc2336cec78ef74c0", - "GOFLAGS": "-tags=tinygo.riscv,baremetal,linux,arm,tinygo.riscv32,esp32c3,esp,xiao_esp32c3,tinygo,purego,osusergo,math_big_pure_go,gc.conservative,scheduler.tasks,serial.usb,tinygo.unicore" - } -} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dd03e7d --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +Copyright The TinyGo Authors. All rights reserved. + +espradio includes portions of the esp-wifi-sys package, which is +licensed under either of: +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) +or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +at your option. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile index c0c6a34..0a89ae7 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,10 @@ update: update-esp-wifi cp -rp esp-wifi/c/headers blobs cp -rp esp-wifi/c/include blobs cp -rp esp-wifi/esp-wifi-sys-esp32c3/libs blobs/libs/esp32c3 + cp -rp esp-wifi/esp-wifi-sys-esp32s3/libs blobs/libs/esp32s3 + +patch-esp32s3: + go run ./tools/patch_xtensa_literals.go blobs/libs/esp32s3/*.a update-esp-wifi: cd esp-wifi && git pull --rebase origin main diff --git a/README.md b/README.md index b049342..9388831 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # espradio -TinyGo package for using the radio onboard ESP32 for wireless communication. Work in progress. +TinyGo package for using the ESP32 onboard radio for wireless communication. -Already functions on the `esp32c3` processor for WiFi communication. More processors coming soon! +Already works on the `esp32c3` and `esp32s3` processors for WiFi. More processors coming soon! Bluetooth is still in progress. @@ -13,22 +13,23 @@ Bluetooth is still in progress. Shows how to set up a WiFi access point with a DHCP server using the low-level lneto interface. ``` -$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=tinygoap -X main.password=YourPasswordHere" -monitor -stack-size 8kb ./examples/ap +$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=tinygoap -X main.password=YourPasswordHere" -monitor ./examples/ap + code data bss | flash ram + 582004 21988 257010 | 603992 278998 Connected to ESP32-C3 -Flashing: 581584/581584 bytes (100%) +Flashing: 604096/604096 bytes (100%) Connected to /dev/ttyACM0. Press Ctrl-C to exit. SHA-256 comparison failed: -Calculated: dfa22060430ed9033cf25f30ff9d91d940f32fbbf2fffe1abbaec4cd0f8e7389 -Expected: 6d9b75f1612e777d952efe3be8dc8cf33d2b9db4314d8cbfddf6d25a01b9c9cb +Calculated: 8d1512da059d76b08bbfe99c14ab43d792466ee8fd85a365b338a6ad17aa1e2a +Expected: 06fd1290b7002c716ccf8a9df263fb33d5e22b6892e08038cb6a8e8f6b04be5f Attempting to boot anyway... -entry 0x40394dc8 +entry 0x40398b3c ap: enabling radio... ap: starting AP... ap: starting L2 netdev (AP)... ap: creating lneto stack... ap: configuring DHCP server... -ap: AP is running on 192.168.4.1 - connect to tinygoap -ap: rx_cb= 0 rx_drop= 0 +ap: AP is running on 192.168.4.1 - connect to tinygo-ap ap: rx_cb= 0 rx_drop= 0 ap: rx_cb= 0 rx_drop= 0 ap: rx_cb= 0 rx_drop= 0 @@ -40,15 +41,17 @@ ap: rx_cb= 0 rx_drop= 0 Connects to a Wi-Fi network and gets an IP address with DHCP using the low-level lneto interface. ``` -$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=yourssid -X main.password=YourPasswordHere" -monitor -stack-size 8kb ./examples/connect-and-dhcp -Connected to ESP32-C3 -Flashing: 578032/578032 bytes (100%) +$ tinygo flash -target xiao-esp32s3 -ldflags="-X main.ssid=yourssid -X main.password=YourPasswordHere" -monitor ./examples/connect-and-dhcp + code data bss | flash ram + 574233 22304 259184 | 596537 281488 +Connected to ESP32-S3 +Flashing: 596640/596640 bytes (100%) Connected to /dev/ttyACM0. Press Ctrl-C to exit. SHA-256 comparison failed: -Calculated: ac0c75a476a9dc3469142d5191d87eb30b499eb61356567eebc1f524e2d4188b -Expected: 330af5651f0cca07a494b68ab84b5631c68a10f9bd2e5568f7145eb7efcc5a9d +Calculated: 84318f99e1f97b458c8a4afc3237908a2d5be760b42c66b39c03367a96e2ae32 +Expected: c3a26bf70f12f0ffc686e708a86b10548920a08e166a0b14d9e2a8f80e662d2e Attempting to boot anyway... -entry 0x40394c20 +entry 0x4038688c initializing radio... starting radio... connecting to rems ... @@ -56,7 +59,7 @@ connected to rems ! starting L2 netdev... creating lneto stack... starting DHCP... -got IP: 192.168.1.46 +got IP: 192.168.1.241 gateway: 192.168.1.1 DNS: 192.168.1.1 done! @@ -70,7 +73,7 @@ alive Connects to a WiFi access point, calls NTP to obtain the current date/time, then serves a tiny web application using the low-level lneto interface. ``` -$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=yourssid -X main.password=YourPasswordHere" -monitor -stack-size 8kb ./examples/http-app/ +$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=yourssid -X main.password=YourPasswordHere" -monitor ./examples/http-app/ Connected to ESP32-C3 Flashing: 652848/652848 bytes (100%) Connected to /dev/ttyACM0. Press Ctrl-C to exit. @@ -98,7 +101,7 @@ listening on http://192.168.1.46:80 Minimal HTTP server that serves a static webpage using the low-level lneto interface. ``` -$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=yourssid -X main.password=YourPasswordHere" -monitor -stack-size 8kb ./examples/http-static/ +$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=yourssid -X main.password=YourPasswordHere" -monitor 8kb ./examples/http-static/ Connected to ESP32-C3 Flashing: 627504/627504 bytes (100%) Connected to /dev/ttyACM0. Press Ctrl-C to exit. @@ -124,33 +127,37 @@ Got webpage request! Uses the MQTT machine to machine protocol to publish and subscribe to messages with the test.mosquitto.org server. Uses the Go stdlib and the [`natiu-mqtt`](github.com/soypat/natiu-mqtt) package with the `netlink` interface. ``` -$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=yourssid -X main.password=yourpassword" -size short -monitor ./examples/mqtt -code data bss | flash ram -698122 22700 273178 | 720822 295878 +$ tinygo flash -target xiao-esp32c3 -ldflags="-X main.ssid=yourssid -X main.password=yourpassword" -size short -monitor ./examples/mqtt/ + code data bss | flash ram + 701066 22700 279290 | 723766 301990 + Connected to ESP32-C3 -Flashing: 720928/720928 bytes (100%) -Connected to /dev/ttyACM0. Press Ctrl-C to exit. -load:0x4202c6f0,len:0x83900 +Flashing: 723872/723872 bytes (100%) +Connected to /dev/ttyACM0. Press Ctrl-C to exit. +load:0x4202c860,len:0x84310 SHA-256 comparison failed: -Calculated: b7e1d566962176bdc79440051ce901ce1e70a5a236d48f82e83684edef7ccfeb -Expected: 74ff2b47841741b35f58f4091f4da93d7e613fbde43fbc082967d06752da3804 -Attempting to boot anyway... -entry 0x4039828c -Connecting to WiFi... -Connected to WiFi. -ClientId: tinygo-client-TJMEVOCXRJ -Connecting to MQTT broker at test.mosquitto.org:1883 -Subscribed to topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq -Message 160.00Mhz received on topic cpu/freq +Calculated: 6bc25eb465fa7599f460725ba7ca7550c86094cb2addfb1fe513499539e0bdd5 +Expected: c4e21f71423096b6072c04d6e2e0c1c8809ea7dc92c5394e6ebba6a0dea25a79 +Attempting to boot anyway... +entry 0x40398dc4 +Connecting to WiFi... +Connected to WiFi. +ClientId: tinygo-client-BFEIHFDOCT +Connecting to MQTT broker at broker.hivemq.com:1883 +TCP connected to 35.157.137.172:1883 +Sending MQTT CONNECT... +MQTT CONNECT succeeded +Subscribed to topic cpu/usage +Message Random value: 45 received on topic cpu/usage +Message Random value: 54 received on topic cpu/usage +Message Random value: 62 received on topic cpu/usage +Message Random value: 41 received on topic cpu/usage +Message Random value: 68 received on topic cpu/usage +Message Random value: 36 received on topic cpu/usage +Message Random value: 22 received on topic cpu/usage +Message Random value: 73 received on topic cpu/usage +Message Random value: 27 received on topic cpu/usage +Message Random value: 86 received on topic cpu/usage Disconnected from MQTT broker. ``` @@ -159,7 +166,7 @@ Disconnected from MQTT broker. Scans for WiFi access points. ``` -$ tinygo flash -target xiao-esp32c3 -monitor -stack-size 8kb ./examples/scan +$ tinygo flash -target xiao-esp32c3 -monitor ./examples/scan Connected to ESP32-C3 Flashing: 442736/442736 bytes (100%) Connected to /dev/ttyACM0. Press Ctrl-C to exit. @@ -208,4 +215,4 @@ HTTP server listening on http://192.168.1.46:80 This package uses files from the [`esp-wifi-sys`](https://github.com/esp-rs/esp-wifi-sys) package, then copies the needed ones into the `blobs` directory. -To update these dependencies to the latest version, run the `make update` command. This will update the submodule, then copy the needed files. Note that this may break existing functionality requiring changes to TinyGo linker files or other changes. +To update these dependencies to the latest version, run the `make update` command. This will update the submodule, then copy the needed files. Then run `make patch-esp32s3` to patch the blobs for the LLD linker. Note that this may break existing functionality requiring changes to TinyGo linker files or other changes. diff --git a/arena.c b/arena.c index d816535..e6de2ff 100644 --- a/arena.c +++ b/arena.c @@ -2,24 +2,204 @@ #include #include #include -#include -void *espradio_arena_alloc(size_t size) { return malloc(size); } -void *espradio_arena_calloc(size_t n, size_t size) { return calloc(n, size); } -void espradio_arena_free(void *p) { free(p); } +/* Arena allocator: boundary-tag free-list allocator inside a Go-allocated pool. + * + * TinyGo's malloc IS the Go GC allocator (picolibc's malloc/sbrk are not + * compiled). The WiFi blob stores allocated pointers in ROM BSS which the + * GC cannot scan, so individual malloc'd objects get collected → crash. + * + * Solution: Go allocates one large []byte (kept alive by a package-level + * variable) and passes its backing array here. All blob allocations are + * sub-allocated from this pool using a first-fit free-list with boundary + * tags for O(1) coalescing on free. + * + * Block layout: + * [header: size_t] payload ... [footer: size_t] + * The size field = total block size (header+payload+footer). + * Low bit of header = 1 means allocated, 0 means free. + * Free blocks additionally store a next pointer in the first sizeof(void*) + * bytes of the payload area. + * + * Minimum block size = HDR + max(sizeof(void*), ALIGN) + FTR. + */ + +#define ALIGN 8 +#define HDR sizeof(size_t) +#define FTR sizeof(size_t) +#define OVERHEAD (HDR + FTR) +#define MIN_PAYLOAD (sizeof(void *) > ALIGN ? sizeof(void *) : ALIGN) +#define MIN_BLOCK (OVERHEAD + MIN_PAYLOAD) +#define ALLOC_BIT ((size_t)1) + +/* Access helpers */ +#define BLK_HDR(b) (*(size_t *)(b)) +#define BLK_SIZE(b) (BLK_HDR(b) & ~ALLOC_BIT) +#define BLK_ALLOC(b) (BLK_HDR(b) & ALLOC_BIT) +#define BLK_FTR(b) (*(size_t *)((uint8_t *)(b) + BLK_SIZE(b) - FTR)) +#define BLK_PAYLOAD(b) ((void *)((uint8_t *)(b) + HDR)) +#define PAYLOAD_BLK(p) ((uint8_t *)(p) - HDR) +#define BLK_NEXT(b) ((uint8_t *)((uint8_t *)(b) + BLK_SIZE(b))) +/* Free-list next pointer stored at start of payload */ +#define FREE_NEXT(b) (*(uint8_t **)((uint8_t *)(b) + HDR)) + +static uint8_t *arena_base; +static size_t arena_cap; +static uint8_t *free_list; /* singly-linked list of free blocks */ + +#if 0 +#define ARENA_DBG(...) printf(__VA_ARGS__) +#else +#define ARENA_DBG(...) ((void)0) +#endif + +void espradio_arena_init(uint8_t *base, size_t cap) { + arena_base = base; + arena_cap = cap; + + /* Align base forward */ + uintptr_t misalign = ((uintptr_t)base) & (ALIGN - 1); + if (misalign) { + size_t pad = ALIGN - misalign; + base += pad; + cap -= pad; + } + + /* Round cap down to alignment */ + cap &= ~(size_t)(ALIGN - 1); + + arena_base = base; + arena_cap = cap; + + /* Initialise entire pool as one free block */ + BLK_HDR(base) = cap; /* size, ALLOC_BIT=0 → free */ + BLK_FTR(base) = cap; + FREE_NEXT(base) = NULL; + free_list = base; +} + +void *espradio_arena_alloc(size_t size) { + if (size == 0) size = 1; + /* Round payload up to alignment */ + size = (size + ALIGN - 1) & ~(size_t)(ALIGN - 1); + size_t need = size + OVERHEAD; + if (need < MIN_BLOCK) need = MIN_BLOCK; + + /* First-fit search */ + uint8_t **prev = &free_list; + uint8_t *blk = free_list; + while (blk) { + size_t bsz = BLK_SIZE(blk); + if (bsz >= need) { + /* Try to split */ + size_t rem = bsz - need; + if (rem >= MIN_BLOCK) { + /* Shrink this free block, allocate from the end */ + BLK_HDR(blk) = rem; /* free block is now smaller */ + BLK_FTR(blk) = rem; + uint8_t *alloc_blk = blk + rem; + BLK_HDR(alloc_blk) = need | ALLOC_BIT; + BLK_FTR(alloc_blk) = need | ALLOC_BIT; + ARENA_DBG("arena: alloc %zu @ %p (split, free_rem=%zu)\n", + size, BLK_PAYLOAD(alloc_blk), rem); + return BLK_PAYLOAD(alloc_blk); + } else { + /* Use entire block */ + *prev = FREE_NEXT(blk); + BLK_HDR(blk) = bsz | ALLOC_BIT; + BLK_FTR(blk) = bsz | ALLOC_BIT; + ARENA_DBG("arena: alloc %zu @ %p (exact %zu)\n", + size, BLK_PAYLOAD(blk), bsz); + return BLK_PAYLOAD(blk); + } + } + prev = (uint8_t **)((uint8_t *)blk + HDR); /* &FREE_NEXT(blk) */ + blk = FREE_NEXT(blk); + } + ARENA_DBG("arena: alloc %zu FAILED (OOM)\n", size); + return NULL; +} + +void *espradio_arena_calloc(size_t n, size_t size) { + size_t total = n * size; + void *p = espradio_arena_alloc(total); + if (p) memset(p, 0, total); + return p; +} + +void espradio_arena_free(void *p) { + if (!p) return; + uint8_t *blk = PAYLOAD_BLK(p); + size_t bsz = BLK_SIZE(blk); + + ARENA_DBG("arena: free %p (blk %p, size %zu)\n", p, blk, bsz); + + /* Coalesce with next block */ + uint8_t *next = blk + bsz; + if (next < arena_base + arena_cap && !BLK_ALLOC(next)) { + /* Remove next from free list */ + uint8_t **pp = &free_list; + while (*pp && *pp != next) pp = (uint8_t **)(*pp + HDR); + if (*pp == next) *pp = FREE_NEXT(next); + bsz += BLK_SIZE(next); + } + + /* Coalesce with previous block */ + if (blk > arena_base) { + size_t prev_sz = *(size_t *)(blk - FTR) & ~ALLOC_BIT; + uint8_t *prev_blk = blk - prev_sz; + if (prev_blk >= arena_base && !BLK_ALLOC(prev_blk)) { + /* Remove prev from free list */ + uint8_t **pp = &free_list; + while (*pp && *pp != prev_blk) pp = (uint8_t **)(*pp + HDR); + if (*pp == prev_blk) *pp = FREE_NEXT(prev_blk); + blk = prev_blk; + bsz += BLK_SIZE(prev_blk); + } + } + + /* Mark free and add to front of free list */ + BLK_HDR(blk) = bsz; /* ALLOC_BIT=0 */ + BLK_FTR(blk) = bsz; + FREE_NEXT(blk) = free_list; + free_list = blk; +} void *espradio_arena_realloc(void *ptr, size_t new_size) { - if (!ptr) return malloc(new_size); - if (new_size == 0) { free(ptr); return NULL; } - void *np = malloc(new_size); + if (!ptr) return espradio_arena_alloc(new_size); + if (new_size == 0) { espradio_arena_free(ptr); return NULL; } + + uint8_t *blk = PAYLOAD_BLK(ptr); + size_t old_total = BLK_SIZE(blk); + size_t old_payload = old_total - OVERHEAD; + + void *np = espradio_arena_alloc(new_size); if (np) { - memcpy(np, ptr, new_size); - free(ptr); + memcpy(np, ptr, old_payload < new_size ? old_payload : new_size); + espradio_arena_free(ptr); } return np; } void espradio_arena_stats(uint32_t *used, uint32_t *capacity) { - if (used) *used = 0; - if (capacity) *capacity = 0; + /* Walk all blocks to compute used bytes */ + uint32_t u = 0; + uint8_t *blk = arena_base; + uint8_t *end = arena_base + arena_cap; + while (blk < end) { + size_t bsz = BLK_SIZE(blk); + if (bsz == 0 || bsz > (size_t)(end - blk)) break; + if (BLK_ALLOC(blk)) u += (uint32_t)bsz; + blk += bsz; + } + if (used) *used = u; + if (capacity) *capacity = (uint32_t)arena_cap; } + +#ifdef __XTENSA__ +/* --wrap redirects (ESP32-S3): catch direct malloc calls from blob .a */ +void *__wrap_malloc(size_t size) { return espradio_arena_alloc(size); } +void *__wrap_calloc(size_t n, size_t size) { return espradio_arena_calloc(n, size); } +void __wrap_free(void *p) { espradio_arena_free(p); } +void *__wrap_realloc(void *p, size_t size) { return espradio_arena_realloc(p, size); } +#endif diff --git a/blobs/libs/esp32s3/libbtbb.a b/blobs/libs/esp32s3/libbtbb.a new file mode 100644 index 0000000..01e7e71 Binary files /dev/null and b/blobs/libs/esp32s3/libbtbb.a differ diff --git a/blobs/libs/esp32s3/libbtdm_app.a b/blobs/libs/esp32s3/libbtdm_app.a new file mode 100644 index 0000000..b86f717 Binary files /dev/null and b/blobs/libs/esp32s3/libbtdm_app.a differ diff --git a/blobs/libs/esp32s3/libcoexist.a b/blobs/libs/esp32s3/libcoexist.a new file mode 100644 index 0000000..1933d8b Binary files /dev/null and b/blobs/libs/esp32s3/libcoexist.a differ diff --git a/blobs/libs/esp32s3/libcore.a b/blobs/libs/esp32s3/libcore.a new file mode 100644 index 0000000..b6b1d4c Binary files /dev/null and b/blobs/libs/esp32s3/libcore.a differ diff --git a/blobs/libs/esp32s3/libespnow.a b/blobs/libs/esp32s3/libespnow.a new file mode 100644 index 0000000..f83c78a Binary files /dev/null and b/blobs/libs/esp32s3/libespnow.a differ diff --git a/blobs/libs/esp32s3/libmesh.a b/blobs/libs/esp32s3/libmesh.a new file mode 100644 index 0000000..2f43f16 Binary files /dev/null and b/blobs/libs/esp32s3/libmesh.a differ diff --git a/blobs/libs/esp32s3/libnet80211.a b/blobs/libs/esp32s3/libnet80211.a new file mode 100644 index 0000000..821b062 Binary files /dev/null and b/blobs/libs/esp32s3/libnet80211.a differ diff --git a/blobs/libs/esp32s3/libphy.a b/blobs/libs/esp32s3/libphy.a new file mode 100644 index 0000000..a21bb3f Binary files /dev/null and b/blobs/libs/esp32s3/libphy.a differ diff --git a/blobs/libs/esp32s3/libpp.a b/blobs/libs/esp32s3/libpp.a new file mode 100644 index 0000000..1af309c Binary files /dev/null and b/blobs/libs/esp32s3/libpp.a differ diff --git a/blobs/libs/esp32s3/libprintf.a b/blobs/libs/esp32s3/libprintf.a new file mode 100644 index 0000000..178d902 Binary files /dev/null and b/blobs/libs/esp32s3/libprintf.a differ diff --git a/blobs/libs/esp32s3/libregulatory.a b/blobs/libs/esp32s3/libregulatory.a new file mode 100644 index 0000000..0f42b7f Binary files /dev/null and b/blobs/libs/esp32s3/libregulatory.a differ diff --git a/blobs/libs/esp32s3/libsmartconfig.a b/blobs/libs/esp32s3/libsmartconfig.a new file mode 100644 index 0000000..7d0e424 Binary files /dev/null and b/blobs/libs/esp32s3/libsmartconfig.a differ diff --git a/blobs/libs/esp32s3/libwapi.a b/blobs/libs/esp32s3/libwapi.a new file mode 100644 index 0000000..7256c31 Binary files /dev/null and b/blobs/libs/esp32s3/libwapi.a differ diff --git a/blobs/libs/esp32s3/libwpa_supplicant.a b/blobs/libs/esp32s3/libwpa_supplicant.a new file mode 100644 index 0000000..6ae424e Binary files /dev/null and b/blobs/libs/esp32s3/libwpa_supplicant.a differ diff --git a/error.go b/error.go index f022840..dbb1380 100644 --- a/error.go +++ b/error.go @@ -1,6 +1,7 @@ package espradio /* +#cgo CFLAGS: -fno-short-enums #include "include.h" */ import "C" diff --git a/esp32c3/esp_phy_adapter.c b/esp32c3/esp_phy_adapter.c index c72ce1e..2e33489 100644 --- a/esp32c3/esp_phy_adapter.c +++ b/esp32c3/esp_phy_adapter.c @@ -352,7 +352,7 @@ void phy_ant_clr_update_flag(void) { * Handles calibration, PLL tracking and antenna configuration. * Power/clock domain is controlled via esp_wifi_bt_power_domain_on(). */ -void esp_phy_enable(uint32_t modem) { +void esp_phy_enable(esp_phy_modem_t modem) { espradio_phy_lock(); uint32_t modem_flags = phy_get_modem_flag(); PHY_ADAPTER_DBG("espradio: esp_phy_enable modem=%lu flags=%lu calibrated=%u\n", @@ -380,7 +380,7 @@ void esp_phy_enable(uint32_t modem) { /* High-level entry point used by IDF to disable PHY for a modem * and power down RF / clocks when the last modem is turned off. */ -void esp_phy_disable(uint32_t modem) { +void esp_phy_disable(esp_phy_modem_t modem) { espradio_phy_lock(); phy_clr_modem_flag(modem); if (phy_get_modem_flag() == 0u) { diff --git a/esp32s3/esp32s3.go b/esp32s3/esp32s3.go new file mode 100644 index 0000000..0c4f431 --- /dev/null +++ b/esp32s3/esp32s3.go @@ -0,0 +1,110 @@ +//go:build esp32s3 + +package esp32s3 + +// #cgo CFLAGS: -I../blobs/include +// #cgo CFLAGS: -I../blobs/include/esp32s3 +// #cgo CFLAGS: -I../blobs/include/local +// #cgo CFLAGS: -I../blobs/headers +// #cgo CFLAGS: -I.. +// #cgo CFLAGS: -DCONFIG_SOC_WIFI_NAN_SUPPORT=0 +// #cgo CFLAGS: -DESPRADIO_PHY_PATCH_ROMFUNCS=0 +// #cgo CFLAGS: -fno-short-enums +import "C" + +import ( + "device/esp" + "sync/atomic" + "unsafe" +) + +const ( + halSystemWiFiClkEn = 0x00FB9FCF + halWiFiBbRstBit = 1 << 0 + halWiFiFeRstBit = 1 << 1 + halWiFiMacRstBit = 1 << 2 + halBtBbRstBit = 1 << 3 // Bluetooth baseband + halBtMacRstBit = 1 << 4 // Bluetooth MAC (deprecated, but pulsed by ESP-IDF) + halRwBtMacRstBit = 1 << 9 // RW Bluetooth MAC + halRwBtMacRegRst = 1 << 11 // RW Bluetooth MAC registers + halWiFiResetMask = halWiFiBbRstBit | halWiFiFeRstBit | halWiFiMacRstBit | + halBtBbRstBit | halBtMacRstBit | halRwBtMacRstBit | halRwBtMacRegRst +) + +var halWiFiClockRefcnt atomic.Uint32 + +//export espradio_hal_init_clocks_go +func espradio_hal_init_clocks_go() { + if halWiFiClockRefcnt.Add(1) != 1 { + return + } + + esp.RTC_CNTL.DIG_PWC.ClearBits(1 << 17) // WIFI_FORCE_PD + esp.APB_CTRL.WIFI_RST_EN.SetBits(halWiFiResetMask) + esp.APB_CTRL.WIFI_RST_EN.ClearBits(halWiFiResetMask) + esp.RTC_CNTL.DIG_ISO.ClearBits(1 << 28) // WIFI_FORCE_ISO + cur := esp.APB_CTRL.GetWIFI_CLK_EN() + esp.APB_CTRL.SetWIFI_CLK_EN(cur | halSystemWiFiClkEn) +} + +//export espradio_hal_disable_clocks_go +func espradio_hal_disable_clocks_go() { + for { + curRef := halWiFiClockRefcnt.Load() + if curRef == 0 { + return + } + if halWiFiClockRefcnt.CompareAndSwap(curRef, curRef-1) { + if curRef != 1 { + return + } + break + } + } + + cur := esp.APB_CTRL.GetWIFI_CLK_EN() + esp.APB_CTRL.SetWIFI_CLK_EN(cur &^ halSystemWiFiClkEn) + esp.RTC_CNTL.DIG_PWC.SetBits(1 << 17) // WIFI_FORCE_PD + esp.RTC_CNTL.DIG_ISO.SetBits(1 << 28) // WIFI_FORCE_ISO +} + +//export espradio_hal_wifi_rtc_enable_iso_go +func espradio_hal_wifi_rtc_enable_iso_go() { + esp.RTC_CNTL.DIG_ISO.SetBits(1 << 28) // WIFI_FORCE_ISO +} + +//export espradio_hal_wifi_rtc_disable_iso_go +func espradio_hal_wifi_rtc_disable_iso_go() { + esp.RTC_CNTL.DIG_ISO.ClearBits(1 << 28) // WIFI_FORCE_ISO +} + +//export espradio_hal_reset_wifi_mac_go +func espradio_hal_reset_wifi_mac_go() { + esp.APB_CTRL.WIFI_RST_EN.SetBits(halWiFiMacRstBit) + esp.APB_CTRL.WIFI_RST_EN.ClearBits(halWiFiMacRstBit) +} + +//export espradio_hal_read_mac_go +func espradio_hal_read_mac_go(mac *C.uchar, iftype C.uint) C.int { + if mac == nil { + return -1 + } + + w0 := esp.EFUSE.GetRD_MAC_SPI_SYS_0() + w1 := esp.EFUSE.GetRD_MAC_SPI_SYS_1_MAC_1() + + m := (*[6]byte)(unsafe.Pointer(mac)) + m[0] = byte((w1 >> 8) & 0xff) + m[1] = byte(w1 & 0xff) + m[2] = byte((w0 >> 24) & 0xff) + m[3] = byte((w0 >> 16) & 0xff) + m[4] = byte((w0 >> 8) & 0xff) + m[5] = byte(w0 & 0xff) + + if iftype != 0 { + m[0] |= 0x02 + m[5] = byte(uint32(m[5]) + uint32(iftype)) + } + + return 0 +} diff --git a/esp32s3/esp_phy_adapter.c b/esp32s3/esp_phy_adapter.c new file mode 100644 index 0000000..51312cc --- /dev/null +++ b/esp32s3/esp_phy_adapter.c @@ -0,0 +1,472 @@ +//go:build esp32s3 + +#include +#include +#include +#include "../blobs/include/include.h" +#include "esp_phy.h" + +#define PHY_ADAPTER_DBG(...) ((void)0) + +extern void phy_wifi_enable_set(uint8_t enable); +extern void *g_phyFuns; +extern int register_chipv7_phy(const esp_phy_init_data_t *init_data, + esp_phy_calibration_data_t *cal_data, + esp_phy_calibration_mode_t cal_mode); +extern void phy_wakeup_init(void); +extern void phy_close_rf(void); +extern void phy_init_flag(void); +extern void phy_xpd_tsens(void); +extern void phy_init_param_set(uint8_t param); +extern void phy_bbpll_en_usb(bool en); +extern void rom_phy_ant_init(void); +extern void rom_phy_track_pll_cap(void); +extern esp_err_t esp_deep_sleep_register_phy_hook(void (*hook)(void)); + +/* PHY critical section — spinlock-based, matching esp-hal approach. + * The blob calls phy_enter_critical/phy_exit_critical (U symbols in libphy.a) + * to protect PHY register access. We use Xtensa RSIL/WSR.PS to + * disable/restore interrupts, matching esp-hal's single-core approach. */ + +uint32_t phy_enter_critical(void) { + uint32_t old; + __asm__ __volatile__("rsil %0, 3" : "=r"(old)); + return old; +} + +void phy_exit_critical(uint32_t level) { + __asm__ __volatile__("wsr.ps %0; rsync" :: "r"(level)); +} + +static uint32_t s_phy_i2c_saved_ps; + +void phy_i2c_enter_critical(void) { + s_phy_i2c_saved_ps = phy_enter_critical(); +} + +void phy_i2c_exit_critical(void) { + phy_exit_critical(s_phy_i2c_saved_ps); +} + +/* ---------- PHY ROM function table ---------- + * + * The ROM section .data_phyrom (VMA 0x3fcef3d4, 0x298 bytes) contains: + * [0x000] rom_phyFuns pointer = 0x3fcef3d8 (points to the table below) + * [0x004..0x298] g_phyFuns_instance: 165 ROM function pointers + * + * The ROM bootloader initialises this from its own flash image, but TinyGo's + * startup code zeros all application DRAM which includes this region. We must + * restore the ROM initial values before calling register_chipv7_phy. + * + * Values extracted from esp32s3_rev0_rom.elf, section .data_phyrom, + * file offset 0x062128, size 0x298 (166 words). */ + +/* Address in ROM DRAM where phy_get_romfuncs() reads the table pointer from. */ +#define PHY_ROM_FUNCS_PTR_ADDR ((volatile uint32_t *)0x3fcef3d4) +/* The ROM's PHY function table in DRAM (g_phyFuns_instance). */ +#define PHY_ROM_FUNCS_INSTANCE ((uint32_t *)0x3fcef3d8) + +static const uint32_t s_phyrom_data_init[166] = { + 0x3fcef3d8, /* [0x000] ptr → INSTANCE */ + 0x40037fa8, 0x40037fcc, 0x40038020, /* [0x004] INSTANCE[0..2] */ + 0x40038068, 0x400380b4, 0x400380e0, 0x40055cc8, /* [0x010] */ + 0x400380f8, 0x40038154, 0x40038194, 0x40055cdc, /* [0x020] */ + 0x400381d8, 0x40055cec, 0x40055d08, 0x40055d24, /* [0x030] */ + 0x40038210, 0x40038254, 0x40038298, 0x400382c4, /* [0x040] */ + 0x40038348, 0x40055d70, 0x400385cc, 0x400386f8, /* [0x050] */ + 0x40038fc8, 0x40038730, 0x4003900c, 0x40039034, /* [0x060] */ + 0x40039074, 0x40038750, 0x4003879c, 0x400387e0, /* [0x070] */ + 0x40038884, 0x4003889c, 0x400390fc, 0x400388e0, /* [0x080] */ + 0x40038904, 0x40038928, 0x40038940, 0x40055d4c, /* [0x090] slot 0x98=0x40038940 */ + 0x40038974, 0x400389f0, 0x40038b00, 0x40038b44, /* [0x0a0] */ + 0x40038c5c, 0x40038cfc, 0x40038d0c, 0x40038d4c, /* [0x0b0] */ + 0x40039168, 0x40038d80, 0x40038db4, 0x40038e60, /* [0x0c0] */ + 0x40038f60, 0x400391c0, 0x40038eac, 0x40038f38, /* [0x0d0] */ + 0x40055bc8, 0x40036438, 0x40036470, 0x40036508, /* [0x0e0] */ + 0x40055bf4, 0x4003658c, 0x4003660c, 0x40036648, /* [0x0f0] */ + 0x400366c0, 0x40036714, 0x40036794, 0x40036804, /* [0x100] */ + 0x40036820, 0x400368c4, 0x4003696c, 0x400369ec, /* [0x110] */ + 0x40055bfc, 0x40036a18, 0x40036aa4, 0x40036ac8, /* [0x120] */ + 0x40036afc, 0x40036b44, 0x40036b98, 0x40055c1c, /* [0x130] */ + 0x40036bc8, 0x40036bec, 0x40055c70, 0x40036c28, /* [0x140] */ + 0x40036ce0, 0x40036d1c, 0x40035474, 0x40035494, /* [0x150] */ + 0x400354bc, 0x40055bb8, 0x40055bc0, 0x400354fc, /* [0x160] slots 0x160/0x164 patched below */ + 0x40035548, 0x40035594, 0x400355d8, 0x40035614, /* [0x170] */ + 0x40035684, 0x400356f0, 0x40035738, 0x400357f8, /* [0x180] */ + 0x40035818, 0x40035880, 0x4003589c, 0x400358d8, /* [0x190] */ + 0x40035964, 0x40035a2c, 0x40035a78, 0x40035acc, /* [0x1a0] */ + 0x40035b28, 0x40035b6c, 0x40035b80, 0x40035b94, /* [0x1b0] */ + 0x40035bf8, 0x40035c2c, 0x40035c80, 0x40035cd4, /* [0x1c0] */ + 0x40035d60, 0x40035dc4, 0x40035e3c, 0x40035e7c, /* [0x1d0] */ + 0x40035eac, 0x40035f44, 0x4003600c, 0x40036020, /* [0x1e0] */ + 0x4003603c, 0x4003605c, 0x400360d4, 0x40036120, /* [0x1f0] */ + 0x40036184, 0x40036210, 0x40036230, 0x4003627c, /* [0x200] slots 0x204/0x208 */ + 0x40036d50, 0x40036d94, 0x40036e78, 0x40036f1c, /* [0x210] */ + 0x40036fc4, 0x40055c7c, 0x40037050, 0x40037118, /* [0x220] */ + 0x400371f0, 0x40037254, 0x4003726c, 0x400372c0, /* [0x230] slot 0x234 */ + 0x40037310, 0x40037348, 0x40037370, 0x4003737c, /* [0x240] */ + 0x400373fc, 0x40037498, 0x40037600, 0x400377a4, /* [0x250] */ + 0x40037804, 0x40037854, 0x40037894, 0x40037958, /* [0x260] */ + 0x40037a44, 0x40037a7c, 0x40037b08, 0x40037c34, /* [0x270] */ + 0x40037c70, 0x40037c88, 0x40037d28, 0x40037d74, /* [0x280] */ + 0x40037d8c, 0x40037dec, /* [0x290] */ +}; + +static void espradio_init_phy_funcs_table(void) { + /* Enable I2C analog master paths needed by PHY calibration. + * ANA_CONFIG_REG (0x6000E044): clear bit 17 = BBPLL I2C, clear bit 18 = SAR I2C. + * ANA_CONFIG2_REG (0x6000E048): set bit 16 = SAR ADC config. + * RTC_CNTL_OPTIONS0_REG: clear bb_i2c_force_pd, bbpll_force_pd, bbpll_i2c_force_pd. */ + #define ANA_CONFIG_REG (*(volatile uint32_t *)0x6000E044) + #define ANA_CONFIG2_REG (*(volatile uint32_t *)0x6000E048) + #define RTC_CNTL_OPTIONS0 (*(volatile uint32_t *)0x60008000) + ANA_CONFIG_REG &= ~((1u << 17) | (1u << 18)); + ANA_CONFIG2_REG |= (1u << 16); + RTC_CNTL_OPTIONS0 &= ~((1u << 6) | (1u << 8) | (1u << 10)); + + /* Restore the ROM .data_phyrom region that TinyGo startup zeroed. + * This restores both the table pointer at 0x3fcef3d4 and the 165-entry + * function table starting at 0x3fcef3d8 with ROM function addresses. + * register_chipv7_phy → phy_get_romfunc_addr will then overwrite ~31 + * slots with IRAM addresses from libphy.a; all other slots keep their + * ROM addresses (e.g. slot 0x98 used by phy_rfcal_data_check). */ + memcpy((void *)0x3fcef3d4, s_phyrom_data_init, sizeof(s_phyrom_data_init)); + + /* Patch the two critical-section slots with our OS-aware versions. */ + PHY_ROM_FUNCS_INSTANCE[0x160 / 4] = (uint32_t)(uintptr_t)phy_enter_critical; + PHY_ROM_FUNCS_INSTANCE[0x164 / 4] = (uint32_t)(uintptr_t)phy_exit_critical; + g_phyFuns = PHY_ROM_FUNCS_INSTANCE; +} + +/* Weak stubs for NVS-based PHY calibration and deep sleep hook registration. */ +__attribute__((weak)) esp_err_t esp_phy_load_cal_data_from_nvs(esp_phy_calibration_data_t *out_cal_data) { + (void)out_cal_data; + return ESP_ERR_NOT_FOUND; +} + +__attribute__((weak)) esp_err_t esp_phy_store_cal_data_to_nvs(const esp_phy_calibration_data_t *cal_data) { + (void)cal_data; + return ESP_OK; +} + +__attribute__((weak)) esp_err_t esp_deep_sleep_register_phy_hook(void (*hook)(void)) { + (void)hook; + return ESP_OK; +} + +extern void espradio_hal_init_clocks_go(void); +extern void espradio_hal_disable_clocks_go(void); + +static volatile uint32_t s_wifi_bt_pd_lock; + +void esp_wifi_bt_power_domain_on(void) { + while (__sync_lock_test_and_set(&s_wifi_bt_pd_lock, 1U)) {} + espradio_hal_init_clocks_go(); + __sync_lock_release(&s_wifi_bt_pd_lock); +} + +void esp_wifi_bt_power_domain_off(void) { + while (__sync_lock_test_and_set(&s_wifi_bt_pd_lock, 1U)) {} + espradio_hal_disable_clocks_go(); + __sync_lock_release(&s_wifi_bt_pd_lock); +} + +extern void phy_param_track_tot(uint32_t wifi_track_pll, uint32_t ble_154_track_pll); +extern uint8_t phy_dig_reg_backup(bool backup_en, uint32_t *mem_addr); +void *heap_caps_malloc(size_t size, uint32_t caps); +extern int rtc_get_reset_reason(int cpu_no); +extern int espradio_hal_read_mac_go(unsigned char *mac, unsigned int iftype); +static uint8_t s_is_phy_calibrated; +static uint8_t s_phy_modem_init_ref; +static esp_phy_calibration_data_t s_phy_cal_data; +static volatile uint32_t s_phy_spin_lock; +static uint16_t s_phy_modem_flags_local; +static uint32_t s_phy_track_pll_started_local; +static uint32_t s_phy_debug_once; +static uint8_t s_phy_ant_need_update_local = 1u; +static uint32_t *s_phy_digital_regs_mem_ptr; +static uint8_t s_phy_is_digital_regs_stored_local; +static esp_timer_handle_t s_phy_track_pll_timer; +static int64_t s_wifi_prev_timestamp_local; +static esp_phy_ant_config_t s_phy_ant_config_local = { + .rx_ant_mode = ESP_PHY_ANT_MODE_ANT0, + .rx_ant_default = ESP_PHY_ANT_ANT0, + .tx_ant_mode = ESP_PHY_ANT_MODE_ANT0, + .enabled_ant0 = 0, + .enabled_ant1 = 1, +}; + +/* Static PHY init data blob (128 bytes for ESP32-S3). + * Values from esp-hal PHY_INIT_DATA_DEFAULT with CONFIG_ESP32_PHY_MAX_TX_POWER=20. + * Same as ESP32-C3 — both use identical PHY init data layout. + */ +static const esp_phy_init_data_t phy_init_data = { .params = { + 0x00, 0x00, 0x50, 0x50, 0x50, 0x4c, 0x4c, 0x48, /* 0 - 7 */ + 0x4c, 0x48, 0x48, 0x44, 0x4a, 0x46, 0x46, 0x42, /* 8 - 15 */ + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, /* 16 - 23 */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 24 - 31 */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 32 - 39 */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 40 - 47 */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 48 - 55 */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 56 - 63 */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, /* 64 - 71 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 72 - 79 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 80 - 87 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 88 - 95 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 96 - 103 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, /* 104 - 111 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 112 - 119 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 120 - 127 */ +} }; + + + +void esp_phy_load_cal_and_init(void) { + const esp_phy_init_data_t *init_data = &phy_init_data; + if (init_data == NULL) { + return; + } + void *espradio_arena_calloc(size_t, size_t); + esp_phy_calibration_data_t *cal_data = (esp_phy_calibration_data_t *)espradio_arena_calloc(1, sizeof(esp_phy_calibration_data_t)); + if (cal_data == NULL) { + cal_data = &s_phy_cal_data; + memset(cal_data, 0, sizeof(*cal_data)); + } + int rr = rtc_get_reset_reason(0); + phy_init_param_set(1u); + bool bbpll_usb = true; + phy_bbpll_en_usb(bbpll_usb); + PHY_ADAPTER_DBG("espradio: phy_bbpll_en_usb=%u reset_reason=%d\n", + (unsigned)(bbpll_usb ? 1u : 0u), rr); + bool force_cal_none = false; /* USB reset (21) needs calibration just like cold boot */ + esp_phy_calibration_mode_t cal_mode = force_cal_none + ? PHY_RF_CAL_NONE + : (esp_phy_calibration_mode_t)(rr == 5 ? PHY_RF_CAL_NONE : PHY_RF_CAL_FULL); + + esp_err_t nvs_rc = esp_phy_load_cal_data_from_nvs(cal_data); + if (nvs_rc != ESP_OK && !force_cal_none) { + cal_mode = PHY_RF_CAL_FULL; + } + (void)espradio_hal_read_mac_go((unsigned char *)cal_data->mac, 0); + espradio_init_phy_funcs_table(); + int rc = register_chipv7_phy(init_data, cal_data, cal_mode); + /* phy_get_romfunc_addr (inside register_chipv7_phy) already updated + * g_phyFuns = *PHY_ROM_FUNCS_PTR_ADDR = PHY_ROM_FUNCS_INSTANCE. */ + g_phyFuns = PHY_ROM_FUNCS_INSTANCE; + if (cal_mode != PHY_RF_CAL_NONE && (nvs_rc != ESP_OK || rc == 1)) { + esp_phy_store_cal_data_to_nvs(cal_data); + } + if (esp_deep_sleep_register_phy_hook(phy_close_rf) == ESP_OK && + esp_deep_sleep_register_phy_hook(phy_xpd_tsens) == ESP_OK && + cal_data != &s_phy_cal_data) { + void espradio_arena_free(void *); + espradio_arena_free(cal_data); + } +} + +static void espradio_phy_lock(void) { + while (__sync_lock_test_and_set(&s_phy_spin_lock, 1u)) { + } +} + +static void espradio_phy_unlock(void) { + __sync_lock_release(&s_phy_spin_lock); +} + +void esp_phy_common_clock_enable(void) { + (void)0; +} + +void esp_phy_common_clock_disable(void) { + (void)0; +} + +static uint32_t phy_enabled_modem_contains_local(uint32_t modem) { + return (uint32_t)((s_phy_modem_flags_local & (uint16_t)modem) != 0u); +} + +static void phy_track_pll_internal_local(void) { + if (phy_enabled_modem_contains_local(1u) == 0u) { + return; + } + s_wifi_prev_timestamp_local = esp_timer_get_time(); + phy_param_track_tot(1u, 0u); +} + +static void phy_track_pll_timer_callback_local(void *arg) { + (void)arg; + espradio_phy_lock(); + phy_track_pll_internal_local(); + espradio_phy_unlock(); +} + +void phy_track_pll_init(void) { + esp_timer_create_args_t args = { + .callback = phy_track_pll_timer_callback_local, + .arg = NULL, + .dispatch_method = ESP_TIMER_TASK, + .name = "phy_track_pll_t", + .skip_unhandled_events = false, + }; + if (esp_timer_create(&args, &s_phy_track_pll_timer) == ESP_OK) { + (void)esp_timer_start_periodic(s_phy_track_pll_timer, 1000000ULL); + s_phy_track_pll_started_local = 1u; + } +} + +void phy_track_pll_deinit(void) { + if (s_phy_track_pll_timer != NULL) { + (void)esp_timer_stop(s_phy_track_pll_timer); + (void)esp_timer_delete(s_phy_track_pll_timer); + s_phy_track_pll_timer = NULL; + } + s_phy_track_pll_started_local = 0u; +} + +void phy_track_pll(void) { + if ((s_phy_track_pll_started_local != 0u) && (phy_enabled_modem_contains_local(1u) != 0u)) { + int64_t now = esp_timer_get_time(); + if ((now - s_wifi_prev_timestamp_local) > 1000000LL) { + phy_track_pll_internal_local(); + } + } +} + +void phy_digital_regs_load(void) { + if ((s_phy_is_digital_regs_stored_local != 0u) && + (s_phy_digital_regs_mem_ptr != NULL)) { + (void)phy_dig_reg_backup(false, s_phy_digital_regs_mem_ptr); + } +} + +void phy_digital_regs_store(void) { + if (s_phy_digital_regs_mem_ptr != NULL) { + (void)phy_dig_reg_backup(true, s_phy_digital_regs_mem_ptr); + s_phy_is_digital_regs_stored_local = 1u; + } +} + +void phy_set_modem_flag(uint32_t modem) { + s_phy_modem_flags_local = (uint16_t)(s_phy_modem_flags_local | (uint16_t)modem); +} + +void phy_clr_modem_flag(uint32_t modem) { + s_phy_modem_flags_local = (uint16_t)(s_phy_modem_flags_local & (uint16_t)(~(uint16_t)modem)); +} + +uint32_t phy_get_modem_flag(void) { + return (uint32_t)s_phy_modem_flags_local; +} + +void esp_phy_modem_init(void) { + espradio_phy_lock(); + s_phy_modem_init_ref = (uint8_t)(s_phy_modem_init_ref + 1u); + if (s_phy_digital_regs_mem_ptr == NULL) { + s_phy_digital_regs_mem_ptr = (uint32_t *)heap_caps_malloc(0x54u, 0x808u); + } + espradio_phy_unlock(); +} + +void *heap_caps_malloc(size_t size, uint32_t caps) { + (void)caps; + void *espradio_arena_alloc(size_t); + return espradio_arena_alloc(size); +} + +void esp_phy_modem_deinit(void) { + espradio_phy_lock(); + uint8_t prev_ref = s_phy_modem_init_ref; + s_phy_modem_init_ref = (uint8_t)(s_phy_modem_init_ref - 1u); + if (prev_ref == 1u) { + s_phy_is_digital_regs_stored_local = 0u; + if (s_phy_digital_regs_mem_ptr != NULL) { + void espradio_arena_free(void *); + espradio_arena_free(s_phy_digital_regs_mem_ptr); + } + s_phy_digital_regs_mem_ptr = NULL; + phy_init_flag(); + } + espradio_phy_unlock(); +} + +bool phy_ant_need_update(void) { + return s_phy_ant_need_update_local != 0u; +} + +void phy_ant_update(void) { + uint32_t ant0 = (uint32_t)s_phy_ant_config_local.enabled_ant0 & 0x0fu; + uint32_t ant1 = (uint32_t)s_phy_ant_config_local.enabled_ant1 & 0x0fu; + uint32_t rx_ant0 = ant0; + uint32_t rx_ant1 = ant0; + uint32_t rx_auto = 0u; + + if (s_phy_ant_config_local.rx_ant_mode == ESP_PHY_ANT_MODE_ANT1) { + rx_ant1 = ant1; + rx_auto = 0u; + } else { + rx_ant1 = ant1; + if (s_phy_ant_config_local.rx_ant_mode == ESP_PHY_ANT_MODE_AUTO) { + rx_auto = 1u; + } else { + rx_auto = 0u; + rx_ant1 = ant0; + } + } + + uint32_t tx_ant0 = ant1; + if (s_phy_ant_config_local.tx_ant_mode != ESP_PHY_ANT_MODE_ANT1) { + tx_ant0 = ant0; + } + + ant_dft_cfg(s_phy_ant_config_local.rx_ant_default == ESP_PHY_ANT_ANT1); + ant_tx_cfg((uint8_t)tx_ant0); + ant_rx_cfg(rx_auto != 0u, (uint8_t)rx_ant0, (uint8_t)rx_ant1); +} + +void phy_ant_clr_update_flag(void) { + s_phy_ant_need_update_local = 0u; +} + +void esp_phy_enable(esp_phy_modem_t modem) { + espradio_phy_lock(); + /* Re-assert g_phyFuns on every enable — BSS can be zeroed between scans. */ + g_phyFuns = PHY_ROM_FUNCS_INSTANCE; + uint32_t modem_flags = phy_get_modem_flag(); + PHY_ADAPTER_DBG("espradio: esp_phy_enable modem=%lu flags=%lu calibrated=%u\n", + (unsigned long)modem, (unsigned long)modem_flags, (unsigned)s_is_phy_calibrated); + if (modem_flags == 0u) { + if (s_is_phy_calibrated == 0u) { + esp_phy_load_cal_and_init(); + s_is_phy_calibrated = 1u; + } else { + PHY_ADAPTER_DBG("espradio: esp_phy_enable phy_wakeup_init\n"); + phy_wakeup_init(); + phy_digital_regs_load(); + } + phy_track_pll_init(); + if (phy_ant_need_update()) { + phy_ant_update(); + phy_ant_clr_update_flag(); + } + } + phy_set_modem_flag(modem); + phy_track_pll(); + espradio_phy_unlock(); +} + +void esp_phy_disable(esp_phy_modem_t modem) { + espradio_phy_lock(); + phy_clr_modem_flag(modem); + if (phy_get_modem_flag() == 0u) { + phy_track_pll_deinit(); + phy_digital_regs_store(); + phy_close_rf(); + phy_xpd_tsens(); + /* Force full re-init (register_chipv7_phy) on the next esp_phy_enable + * instead of phy_wakeup_init. phy_wakeup_init dispatches through + * g_phyFuns noop entries that fail to restore RF state, causing MAC + * interrupt storms on scan 2+. */ + s_is_phy_calibrated = 0u; + } + espradio_phy_unlock(); +} diff --git a/esp32s3/isr.c b/esp32s3/isr.c new file mode 100644 index 0000000..6381c4e --- /dev/null +++ b/esp32s3/isr.c @@ -0,0 +1,85 @@ +//go:build esp32s3 + +#include +#include "espradio.h" +#include "soc/interrupts.h" + +/* ---- Xtensa interrupt controller (ESP32-S3) ---- */ + +/* On Xtensa the interrupt enable/disable is done via special registers + * (INTENABLE) using RSR/WSR instructions. The interrupt matrix routes + * peripheral sources to CPU interrupt lines. */ + +/* The CPU interrupt number used for all WiFi peripheral sources. + * On Xtensa, TinyGo's handleInterrupt() only dispatches lines 6-30. + * Interrupt 12 is ExternLevel at level 1 (level-triggered, suitable for + * WiFi MAC which holds its interrupt line high until acknowledged). */ +#define ESPRADIO_WIFI_CPU_INT 12u + +/* Pre-wire WiFi peripheral interrupt sources to the WiFi CPU interrupt. + * Must be called before esp_wifi_init so routing is in place before the + * blob enables the peripheral-side interrupts. */ +void espradio_prewire_wifi_interrupts(void) { + /* Map ALL WiFi-related peripheral sources to our CPU interrupt */ + intr_matrix_set(0, ETS_WIFI_MAC_INTR_SOURCE, ESPRADIO_WIFI_CPU_INT); /* src 0 */ + intr_matrix_set(0, ETS_WIFI_MAC_NMI_SOURCE, ESPRADIO_WIFI_CPU_INT); /* src 1 - NMI */ + intr_matrix_set(0, ETS_WIFI_PWR_INTR_SOURCE, ESPRADIO_WIFI_CPU_INT); /* src 2 */ + intr_matrix_set(0, ETS_WIFI_BB_INTR_SOURCE, ESPRADIO_WIFI_CPU_INT); /* src 3 */ +} + +/* No-op: the blob calls set_intr to route peripheral sources to CPU + * interrupts, but routing is already configured by + * espradio_prewire_wifi_interrupts(). */ +void espradio_set_intr(int32_t cpu_no, uint32_t intr_source, uint32_t intr_num, int32_t intr_prio) { + intr_matrix_set(0, intr_source, ESPRADIO_WIFI_CPU_INT); +} + +/* No-op: same as set_intr. */ +void espradio_clear_intr(uint32_t intr_source, uint32_t intr_num) { + (void)intr_source; + (void)intr_num; +} + +/* Enable CPU interrupts using Xtensa INTENABLE special register. + * The blob calls ints_on with its own mask (1<<0 for WiFi MAC). + * We translate: if the blob's mask includes a WiFi-related bit, + * also set our actual WiFi CPU interrupt bit. */ +void espradio_ints_on(uint32_t mask) { + uint32_t actual_mask = mask | (1u << ESPRADIO_WIFI_CPU_INT); + uint32_t val; + __asm__ volatile ("rsr %0, intenable" : "=r"(val)); + val |= actual_mask; + __asm__ volatile ("wsr %0, intenable; rsync" :: "r"(val)); +} + +void espradio_ints_off(uint32_t mask) { + uint32_t val; + __asm__ volatile ("rsr %0, intenable" : "=r"(val)); + val &= ~mask; + __asm__ volatile ("wsr %0, intenable; rsync" :: "r"(val)); +} + +/* On Xtensa, interrupt 1 is already level-triggered (type is determined + * by hardware, not software-configurable like on RISC-V). + * This function is a no-op for ESP32-S3. */ +void espradio_wifi_int_to_level(void) { + /* nothing to do — Xtensa interrupt types are fixed by hardware */ +} + +/* On Xtensa, interrupt priorities are fixed by hardware (interrupt 1 is + * level 1). TinyGo's interrupt.Enable() handles enabling it. + * This is a no-op for ESP32-S3. */ +void espradio_wifi_int_raise_priority(void) { + /* nothing to do — Xtensa interrupt priorities are fixed */ +} + +/* On Xtensa, level-triggered interrupts auto-clear when the peripheral + * de-asserts. We still mask/unmask to prevent re-entry while the + * bottom-half runs. */ +void espradio_wifi_isr_post_mask(void) { + espradio_ints_off(1u << ESPRADIO_WIFI_CPU_INT); +} + +void espradio_wifi_unmask(void) { + espradio_ints_on(1u << ESPRADIO_WIFI_CPU_INT); +} diff --git a/espradio.h b/espradio.h index dcde457..981d4d7 100644 --- a/espradio.h +++ b/espradio.h @@ -3,6 +3,7 @@ #include "include.h" /* ===== Go → C (implemented in top-level .c files) ===== */ +void espradio_arena_init(uint8_t *base, size_t cap); void espradio_set_blob_log_level(uint32_t level); esp_err_t espradio_wifi_init(void); void espradio_wifi_init_completed(void); @@ -18,6 +19,7 @@ void espradio_ensure_osi_ptr(void); void espradio_coex_adapter_init(void); void espradio_call_saved_isr(int32_t n); void espradio_call_wifi_isr(void); +uint32_t espradio_get_wifi_isr_count(void); void espradio_prewire_wifi_interrupts(void); void espradio_wifi_int_to_level(void); void espradio_wifi_int_raise_priority(void); @@ -49,10 +51,13 @@ extern esp_err_t esp_wifi_connect_internal(void); /* ===== netif (netif.c) ===== */ void espradio_netif_init_netstack_cb(void); void espradio_post_start_cb(void); +void espradio_save_rom_ptrs(void); +void espradio_restore_rom_ptrs(void); esp_err_t espradio_netif_start_rx(int ap_mode); int espradio_netif_rx_available(void); uint16_t espradio_netif_rx_pop(void *dst, uint16_t dst_len); int espradio_netif_tx(void *buf, uint16_t len); +void espradio_netif_set_connected(int connected); esp_err_t espradio_netif_get_mac(uint8_t mac[6]); uint32_t espradio_netif_rx_cb_count(void); uint32_t espradio_netif_rx_cb_drop(void); @@ -72,9 +77,9 @@ extern void espradio_hal_reset_wifi_mac_go(void); extern int espradio_hal_read_mac_go(unsigned char *mac, unsigned int iftype); extern void espradio_on_wifi_event(int32_t eventID, void *data); -/* ===== esp32c3/ → linker (implemented in esp32c3/ *.c) ===== */ -extern void esp_phy_enable(uint32_t modem); -extern void esp_phy_disable(uint32_t modem); +/* ===== chip-specific → linker (implemented in esp32c3/ or esp32s3/ *.c) ===== */ +extern void esp_phy_enable(esp_phy_modem_t modem); +extern void esp_phy_disable(esp_phy_modem_t modem); // Interrupt controller / ISR helpers. void intr_matrix_set(uint32_t cpu_no, uint32_t model_num, uint32_t intr_num); diff --git a/examples/mqtt/main.go b/examples/mqtt/main.go index 69b66c7..bcc431b 100644 --- a/examples/mqtt/main.go +++ b/examples/mqtt/main.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "log" - "machine" "math/rand" "net" "time" @@ -20,8 +19,8 @@ import ( var ( ssid string password string - broker string = "test.mosquitto.org:1883" - topic string = "cpu/freq" + broker string = "broker.hivemq.com:1883" + topic string = "cpu/usage" ) func main() { @@ -34,12 +33,24 @@ func main() { clientId := "tinygo-client-" + randomString(10) fmt.Printf("ClientId: %s\n", clientId) - // Get a transport for MQTT packets + // Get a transport for MQTT packets. + // Retry TCP connection since public brokers may reject/close connections under load. fmt.Printf("Connecting to MQTT broker at %s\n", broker) - conn, err := net.Dial("tcp", broker) - if err != nil { - log.Fatal(err) + var conn net.Conn + for attempt := range 5 { + var err error + conn, err = net.Dial("tcp", broker) + if err != nil { + fmt.Printf("net.Dial attempt %d failed: %s\n", attempt+1, err) + time.Sleep(2 * time.Second) + continue + } + break + } + if conn == nil { + log.Fatal("all TCP connection attempts failed") } + fmt.Printf("TCP connected to %v\n", conn.RemoteAddr()) defer conn.Close() // Create new client @@ -55,11 +66,14 @@ func main() { // Connect client var varconn mqtt.VariablesConnect varconn.SetDefaultMQTT([]byte(clientId)) + varconn.KeepAlive = 60 // seconds; some brokers reject KeepAlive=0 + fmt.Println("Sending MQTT CONNECT...") ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) - err = client.Connect(ctx, conn, &varconn) + err := client.Connect(ctx, conn, &varconn) if err != nil { log.Fatal("failed to connect: ", err) } + fmt.Println("MQTT CONNECT succeeded") // Subscribe to topic ctx, _ = context.WithTimeout(context.Background(), 10*time.Second) @@ -85,8 +99,7 @@ func main() { log.Fatal("client disconnected: ", client.Err()) } - freq := float32(machine.CPUFrequency()) / 1000000 - payload := fmt.Sprintf("%.02fMhz", freq) + payload := fmt.Sprintf("Random value: %d", randomInt(0, 100)) pubVar.PacketIdentifier++ err = client.PublishPayload(pubFlags, pubVar, []byte(payload)) diff --git a/examples/scan/main.go b/examples/scan/main.go index de60b2a..34ca77f 100644 --- a/examples/scan/main.go +++ b/examples/scan/main.go @@ -15,9 +15,7 @@ func main() { time.Sleep(time.Second) println("initializing radio...") - err := espradio.Enable(espradio.Config{ - Logging: espradio.LogLevelError, - }) + err := espradio.Enable(espradio.Config{}) if err != nil { println("could not enable radio:", err) return diff --git a/isr.c b/isr.c index ade14cb..6b64659 100644 --- a/isr.c +++ b/isr.c @@ -2,6 +2,78 @@ #include #include +#ifdef __XTENSA__ +#define ESPRADIO_MEMORY_BARRIER() __asm__ volatile ("memw" ::: "memory") +#else +#define ESPRADIO_MEMORY_BARRIER() __asm__ volatile ("fence" ::: "memory") +#endif + +/* ---- User exception handler (Xtensa level-1 vector) ---- */ +#ifdef __XTENSA__ +static const char *exccause_name(uint32_t cause) { + switch (cause) { + case 0: return "IllegalInstruction"; + case 2: return "InstructionFetchError"; + case 3: return "LoadStoreError"; + case 6: return "IntegerDivideByZero"; + case 9: return "LoadStoreAlignment"; + case 12: return "InstructionPIFDataError"; + case 13: return "LoadStorePIFDataError"; + case 14: return "InstructionPIFAddrError"; + case 15: return "LoadStorePIFAddrError"; + case 20: return "InstTLBMiss"; + case 24: return "LoadStoreTLBMiss"; + case 28: return "LoadProhibited"; + case 29: return "StoreProhibited"; + default: return "Unknown"; + } +} + +void espradio_user_exception(uint32_t cause, uint32_t epc, uint32_t excvaddr, uint32_t *frame) { + /* Write crash signature to RTC STORE registers (survive across reset) */ + volatile uint32_t *store0 = (volatile uint32_t *)0x60008050; + volatile uint32_t *store1 = (volatile uint32_t *)0x60008054; + volatile uint32_t *store2 = (volatile uint32_t *)0x60008058; + *store0 = 0x55570000 | (cause & 0xFFFF); /* 0x5557 = user exception marker */ + *store1 = epc; + *store2 = excvaddr; + + printf("\n*** USER EXCEPTION ***\n"); + printf(" EXCCAUSE = %lu (%s)\n", (unsigned long)cause, exccause_name(cause)); + printf(" EPC1 = 0x%08lx\n", (unsigned long)epc); + printf(" EXCVADDR = 0x%08lx\n", (unsigned long)excvaddr); + /* Dump WindowBase and WindowStart to see register window state */ + uint32_t wb, ws; + __asm__ volatile ("rsr %0, WINDOWBASE" : "=r"(wb)); + __asm__ volatile ("rsr %0, WINDOWSTART" : "=r"(ws)); + printf(" WINDOWBASE = %lu WINDOWSTART = 0x%04lx\n", + (unsigned long)wb, (unsigned long)ws); + /* Dump saved registers from the exception frame. + * Layout: 0:a0 4:a1(orig) 8:a2 12:a3 16:a4 20:a5 + * 24:a6 28:a7 32:a8 36:a9 40:a10 44:a11 + * 48:a12 52:a13 56:a14 60:a15 64:SAR 68:EPC1 72:PS */ + if (frame) { + printf(" Saved registers:\n"); + static const char *rn[] = {"a0","a1","a2","a3","a4","a5","a6","a7", + "a8","a9","a10","a11","a12","a13","a14","a15"}; + for (int i = 0; i < 16; i++) { + printf(" %-3s = 0x%08lx\n", rn[i], (unsigned long)frame[i]); + } + printf(" SAR = 0x%08lx PS = 0x%08lx\n", + (unsigned long)frame[16], (unsigned long)frame[18]); + } + fflush(stdout); + printf("*** resetting ***\n"); + fflush(stdout); + /* Trigger software system reset (preserves RTC STORE) */ + volatile uint32_t *options0 = (volatile uint32_t *)0x60008000; + *options0 |= (1u << 31); + for (;;) { + __asm__ volatile ("waiti 0"); + } +} +#endif + /* ---- ISR fn/arg storage ---- */ static void (*s_isr_fn[32])(void *); @@ -18,44 +90,31 @@ void espradio_set_isr(int32_t n, void *f, void *arg) { static volatile uint32_t s_in_isr = 0; -void espradio_call_saved_isr(int32_t n) { - s_in_isr = 1; - __asm__ volatile ("fence" ::: "memory"); - if (n >= 0 && n < 32 && s_isr_fn[n]) { - s_isr_fn[n](s_isr_arg[n]); - } - __asm__ volatile ("fence" ::: "memory"); - s_in_isr = 0; -} - -/* Dispatch all WiFi-related ISR handlers. The blob registers its ISR - * via set_isr(n, fn, arg) where n may be 0 (WIFI_MAC source) or 1. - * In Rust esp-wifi both indices map to a single handler object; here - * we call every non-NULL entry in the WiFi source range (0-3) so that - * no registered handler is missed regardless of which index the blob - * chose. Called from the Go interrupt handler for CPU interrupt 1. */ __attribute__((weak)) void espradio_wifi_isr_post_mask(void) {} +static volatile uint32_t s_wifi_isr_count; void espradio_call_wifi_isr(void) { + s_wifi_isr_count++; s_in_isr = 1; - __asm__ volatile ("fence" ::: "memory"); - for (int i = 0; i < 4; i++) { + ESPRADIO_MEMORY_BARRIER(); + // CALL ALL ISRs from 0 to 31 just in case, to see if they are set! + for (int i = 0; i < 32; i++) { if (s_isr_fn[i]) { s_isr_fn[i](s_isr_arg[i]); } } - __asm__ volatile ("fence" ::: "memory"); + ESPRADIO_MEMORY_BARRIER(); s_in_isr = 0; espradio_wifi_isr_post_mask(); } +uint32_t espradio_get_wifi_isr_count(void) { return s_wifi_isr_count; } bool espradio_is_from_isr(void) { return s_in_isr != 0; } void espradio_task_yield_from_isr(void) { - /* no-op: unsafe to call Go scheduler from ISR context */ } /* ---- ISR ring buffer ---- */ @@ -85,7 +144,7 @@ int32_t espradio_queue_send_from_isr(void *queue, void *item, void *hptw) { } else { __builtin_memset(s_isr_ring_items[head], 0, ESPRADIO_ISR_ITEM_SIZE); } - __asm__ volatile ("fence" ::: "memory"); + ESPRADIO_MEMORY_BARRIER(); s_isr_ring_head = next; return 1; } @@ -93,9 +152,10 @@ int32_t espradio_queue_send_from_isr(void *queue, void *item, void *hptw) { uint32_t espradio_isr_ring_head(void) { return s_isr_ring_head; } uint32_t espradio_isr_ring_tail(void) { return s_isr_ring_tail; } void espradio_isr_ring_advance_tail(void) { - __asm__ volatile ("fence" ::: "memory"); + ESPRADIO_MEMORY_BARRIER(); s_isr_ring_tail = (s_isr_ring_tail + 1u) % ESPRADIO_ISR_RING_SIZE; } void *espradio_isr_ring_entry_queue(uint32_t idx) { return s_isr_ring_queue[idx]; } void *espradio_isr_ring_entry_item(uint32_t idx) { return s_isr_ring_items[idx]; } uint32_t espradio_isr_ring_drops(void) { return s_isr_ring_drops; } + diff --git a/netif.c b/netif.c index 3a1295d..6eb87c1 100644 --- a/netif.c +++ b/netif.c @@ -41,10 +41,46 @@ static void blob_cb_noop(void) { } static uint32_t s_pp_wdev_save[PP_WDEV_FUNCS_ENTRIES]; /* net80211_funcs relocation: same DMA corruption issue as pp_wdev_funcs. - * The blob allocates this table on the heap; we relocate to static .bss. */ -#define NET80211_FUNCS_MAX_ENTRIES 64 + * The blob allocates this table on the heap; we relocate to static .bss. + * net80211_funcs_init writes ~44 entries; 128 provides safe headroom. */ +#define NET80211_FUNCS_MAX_ENTRIES 128 static uint32_t s_net80211_funcs_save[NET80211_FUNCS_MAX_ENTRIES]; +/* g_phyFuns relocation: the PHY function table lives at a fixed DRAM address + * (0x3fcef3d4, size 0x298 = 166 words) that can be corrupted at runtime — + * entries are overwritten with arena allocation addresses, causing + * InstructionFetchError when the PHY calibration timer reads temperature. + * Relocate to static .bss after init and redirect g_phyFuns. */ +#define PHY_FUNCS_TABLE_WORDS 166 +static uint32_t s_phyFuns_save[PHY_FUNCS_TABLE_WORDS]; +extern void *g_phyFuns; + +/* ppCheckTxConnTrafficIdle is called only by PM timer callbacks (pm_dream, + * pm_go_to_wake, pm_send_probe_stop, pm_update_params, etc.). It walks + * TX frame descriptors in the lmac/pTxRx pools, which live in SRAM2 and + * may be in an inconsistent state under cooperative scheduling (the blob + * expects ppTask to run preemptively and finalise frame descriptors before + * PM callbacks access them). With WIFI_PS_NONE the return value is + * irrelevant — wrap it to always return 0 ("idle") and avoid the crash. */ +int __wrap_ppCheckTxConnTrafficIdle(void) { return 0; } + +/* ROM-fixed pointer variables in the 0x3fcef9xx region that are critical for + * TX operations. WiFi DMA can corrupt these the same way it corrupts + * pp_wdev_funcs. We snapshot them after the blob finishes init and restore + * before every schedOnce / TX to prevent stale-DMA-induced crashes. */ +extern volatile uint32_t *pTxRx; /* 0x3fcef954 – set by pp_attach */ +extern volatile uint32_t *our_tx_eb; /* 0x3fcef948 */ +extern volatile uint32_t *our_wait_eb; /* 0x3fcef94c */ +extern volatile uint32_t *lmacConfMib_ptr; /* 0x3fcef950 */ +extern wifi_osi_funcs_t *g_osi_funcs_p; /* 0x3fcef940 */ + +static uint32_t s_saved_pTxRx; +static uint32_t s_saved_our_tx_eb; +static uint32_t s_saved_our_wait_eb; +static uint32_t s_saved_lmacConfMib_ptr; +static uint32_t s_saved_g_osi_funcs_p; +static int s_rom_ptrs_saved; + /* Forward declaration — defined below. */ static esp_err_t espradio_sta_rxcb(void *buffer, uint16_t len, void *eb); @@ -74,6 +110,12 @@ void espradio_netif_init_netstack_cb(void) { esp_wifi_internal_reg_netstack_buf_cb(netstack_buf_ref_noop, netstack_buf_free_noop); espradio_patch_blob_cb_vars(); + + /* Missing initialization steps required before esp_wifi_start() */ + esp_wifi_set_mode(WIFI_MODE_NULL); + esp_wifi_set_tx_done_cb(espradio_tx_done_noop); + esp_wifi_internal_reg_rxcb(WIFI_IF_STA, espradio_sta_rxcb); + esp_wifi_internal_reg_rxcb(WIFI_IF_AP, espradio_sta_rxcb); } /* Called after esp_wifi_start(): re-patch any DRAM variables that @@ -91,7 +133,6 @@ void espradio_post_start_cb(void) { /* Check whether the blob moved g_osi_funcs_p away from our table. */ extern wifi_osi_funcs_t espradio_osi_funcs; - extern wifi_osi_funcs_t *g_osi_funcs_p; extern wifi_osi_funcs_t g_wifi_osi_funcs; extern wifi_osi_funcs_t *s_heap_osi_funcs; @@ -124,6 +165,47 @@ void espradio_post_start_cb(void) { net80211_funcs = s_net80211_funcs_save; } } + + /* Relocate g_phyFuns table from fixed DRAM to static .bss. */ + if (g_phyFuns) { + uint32_t *rom_table = (uint32_t *)g_phyFuns; + for (int i = 0; i < PHY_FUNCS_TABLE_WORDS; i++) + s_phyFuns_save[i] = rom_table[i]; + g_phyFuns = s_phyFuns_save; + } +} + +/* Snapshot the critical ROM pointers after the blob has fully initialised + * them (pp_attach sets pTxRx, lmacInit sets lmacConfMib_ptr, etc.). + * Called from Go after pumping schedOnce enough times for ppTask to + * process the START command. */ +void espradio_save_rom_ptrs(void) { + s_saved_pTxRx = (uint32_t)(uintptr_t)pTxRx; + s_saved_our_tx_eb = (uint32_t)(uintptr_t)our_tx_eb; + s_saved_our_wait_eb = (uint32_t)(uintptr_t)our_wait_eb; + s_saved_lmacConfMib_ptr = (uint32_t)(uintptr_t)lmacConfMib_ptr; + s_saved_g_osi_funcs_p = (uint32_t)(uintptr_t)g_osi_funcs_p; + s_rom_ptrs_saved = 1; +} + +/* Restore the ROM pointers from snapshot. Called from schedOnce (Go side) + * and before every TX to undo any DMA corruption in the ROM data area. */ +void espradio_restore_rom_ptrs(void) { + if (!s_rom_ptrs_saved) return; + if ((uint32_t)(uintptr_t)pTxRx != s_saved_pTxRx) + pTxRx = (volatile uint32_t *)(uintptr_t)s_saved_pTxRx; + if ((uint32_t)(uintptr_t)our_tx_eb != s_saved_our_tx_eb) + our_tx_eb = (volatile uint32_t *)(uintptr_t)s_saved_our_tx_eb; + if ((uint32_t)(uintptr_t)our_wait_eb != s_saved_our_wait_eb) + our_wait_eb = (volatile uint32_t *)(uintptr_t)s_saved_our_wait_eb; + if ((uint32_t)(uintptr_t)lmacConfMib_ptr != s_saved_lmacConfMib_ptr) + lmacConfMib_ptr = (volatile uint32_t *)(uintptr_t)s_saved_lmacConfMib_ptr; + if ((uint32_t)(uintptr_t)g_osi_funcs_p != s_saved_g_osi_funcs_p) + g_osi_funcs_p = (wifi_osi_funcs_t *)(uintptr_t)s_saved_g_osi_funcs_p; + /* esp_phy_enable resets g_phyFuns to the fixed DRAM address on every call. + * Redirect it back to our static copy. */ + if (g_phyFuns != s_phyFuns_save && s_phyFuns_save[0] != 0) + g_phyFuns = s_phyFuns_save; } #define ESPRADIO_NETIF_RXRING_SIZE 8 @@ -158,6 +240,11 @@ static esp_err_t espradio_sta_rxcb(void *buffer, uint16_t len, void *eb) { } static wifi_interface_t s_active_if = WIFI_IF_STA; +static volatile int s_sta_connected; + +void espradio_netif_set_connected(int connected) { + s_sta_connected = connected; +} esp_err_t espradio_netif_start_rx(int ap_mode) { s_active_if = ap_mode ? WIFI_IF_AP : WIFI_IF_STA; @@ -180,6 +267,8 @@ uint16_t espradio_netif_rx_pop(void *dst, uint16_t dst_len) { } int espradio_netif_tx(void *buf, uint16_t len) { + if (!s_sta_connected) return ESP_ERR_WIFI_NOT_CONNECT; + espradio_restore_rom_ptrs(); return esp_wifi_internal_tx(s_active_if, buf, len); } diff --git a/netif_esp.go b/netif_esp.go index c0cacb9..7cbc68f 100644 --- a/netif_esp.go +++ b/netif_esp.go @@ -1,6 +1,7 @@ package espradio /* +#cgo CFLAGS: -fno-short-enums #include "espradio.h" */ import "C" diff --git a/osi.c b/osi.c index 0d6ba36..c67b031 100644 --- a/osi.c +++ b/osi.c @@ -124,7 +124,14 @@ void *espradio_wifi_thread_semphr_get(void); void *espradio_recursive_mutex_create(void); static void *espradio_mutex_create(void) { - return espradio_recursive_mutex_create(); +#if ESPRADIO_OSI_DEBUG + printf("osi: mutex_create\n"); +#endif + void *ret = espradio_recursive_mutex_create(); +#if ESPRADIO_OSI_DEBUG + printf("osi: mutex_create -> %p\n", ret); +#endif + return ret; } void espradio_mutex_delete(void *mutex); @@ -137,20 +144,32 @@ void *espradio_generic_queue_create(uint32_t queue_len, uint32_t item_size); void espradio_generic_queue_delete(void *queue); static void *espradio_queue_create(uint32_t queue_len, uint32_t item_size) { +#if ESPRADIO_OSI_DEBUG + printf("osi: queue_create len=%lu size=%lu\n", (unsigned long)queue_len, (unsigned long)item_size); +#endif return espradio_generic_queue_create(queue_len, item_size); } static void espradio_queue_delete(void *queue) { +#if ESPRADIO_OSI_DEBUG + printf("osi: queue_delete %p\n", queue); +#endif espradio_generic_queue_delete(queue); } int32_t espradio_queue_send(void *queue, void *item, uint32_t block_time_tick); static int32_t espradio_queue_send_to_back(void *queue, void *item, uint32_t block_time_tick) { +#if ESPRADIO_OSI_DEBUG + printf("osi: queue_send_to_back q=%p tick=%lu\n", queue, (unsigned long)block_time_tick); +#endif return espradio_queue_send(queue, item, block_time_tick); } static int32_t espradio_queue_send_to_front(void *queue, void *item, uint32_t block_time_tick) { +#if ESPRADIO_OSI_DEBUG + printf("osi: queue_send_to_front q=%p tick=%lu\n", queue, (unsigned long)block_time_tick); +#endif return espradio_queue_send(queue, item, block_time_tick); } @@ -412,8 +431,21 @@ esp_err_t esp_event_post(esp_event_base_t event_base, int32_t event_id, const vo (unsigned)b0, (unsigned)b1, (unsigned)b2, (unsigned)b3, (unsigned)b4, (unsigned)b5, (unsigned)b6, (unsigned)b7); if (event_base && strcmp(event_base, s_wifi_event_base) == 0 && event_id == 1 && event_data_size >= 6) { - printf("osi: event_post scan_done status=%lu number=%u scan_id=%u\n", - (unsigned long)scan_status, (unsigned)scan_number, (unsigned)scan_id); + extern uint32_t espradio_get_wifi_isr_count(void); + uint32_t intenable, interrupt_reg; + #ifdef __XTENSA__ + __asm__ volatile ("rsr %0, intenable" : "=r"(intenable)); + __asm__ volatile ("rsr %0, interrupt" : "=r"(interrupt_reg)); + #else + intenable = 0; interrupt_reg = 0; + #endif + volatile uint32_t *int_map = (volatile uint32_t *)0x600C2000; + printf("osi: scan_done isr=%lu INTEN=0x%08lx INT=0x%08lx MAC_MAP=%lu BB_MAP=%lu PWR_MAP=%lu\n", + (unsigned long)espradio_get_wifi_isr_count(), + (unsigned long)intenable, (unsigned long)interrupt_reg, + (unsigned long)(int_map[0] & 0x1f), + (unsigned long)(int_map[2] & 0x1f), + (unsigned long)(int_map[3] & 0x1f)); } #endif if (!s_event_loop_ready) return 0; @@ -515,8 +547,12 @@ static void espradio_phy_disable(void) { static void espradio_phy_enable(void) { esp_phy_enable(ESPRADIO_PHY_MODEM_WIFI); phy_wifi_enable_set(1); -#if ESPRADIO_OSI_DEBUG - printf("osi: phy_enable\n"); +#ifdef __XTENSA__ + /* PHY blob may re-enable glitch/brownout detectors; re-disable them. */ + *(volatile uint32_t *)0x60008034 &= ~(1u << 20); /* GLITCH_RST_EN=0 */ + *(volatile uint32_t *)0x60008148 = 0x0; /* FIB_SEL=0 (use register, not eFuse) */ + *(volatile uint32_t *)0x60008144 &= ~(1u << 31); /* POWER_GLITCH_EN=0 */ + *(volatile uint32_t *)0x600080E8 &= ~((1u << 30) | (1u << 26)); /* BOD off */ #endif } @@ -922,6 +958,9 @@ static int espradio_nvs_get_u16(uint32_t handle, const char* key, uint16_t* out_ } static int espradio_nvs_open(const char* name, unsigned int open_mode, uint32_t *out_handle) { +#if ESPRADIO_OSI_DEBUG + printf("osi: nvs_open name=%s mode=%u\n", name ? name : "(null)", open_mode); +#endif (void)name; (void)open_mode; if (!out_handle) return -1; @@ -1025,30 +1064,48 @@ uint32_t espradio_log_timestamp(void); static void * espradio_malloc_internal(size_t size) { espradio_alloc_count++; - return espradio_arena_alloc(size); + void *ret = espradio_arena_alloc(size); +#if ESPRADIO_OSI_DEBUG + printf("osi: malloc_internal %zu -> %p (caller=%p)\n", size, ret, __builtin_return_address(0)); + fflush(stdout); +#endif + return ret; } static void * espradio_realloc_internal(void *ptr, size_t size) { espradio_alloc_count++; - return espradio_arena_realloc(ptr, size); + void *ret = espradio_arena_realloc(ptr, size); +#if ESPRADIO_OSI_DEBUG + printf("osi: realloc_internal %p %zu -> %p\n", ptr, size, ret); +#endif + return ret; } static void * espradio_calloc_internal(size_t n, size_t size) { espradio_alloc_count++; - return espradio_arena_calloc(n, size); + void *ret = espradio_arena_calloc(n, size); +#if ESPRADIO_OSI_DEBUG + printf("osi: calloc_internal %zu*%zu -> %p\n", n, size, ret); +#endif + return ret; } static void * espradio_zalloc_internal(size_t size) { espradio_alloc_count++; - return espradio_arena_calloc(1, size); + void *ret = espradio_arena_calloc(1, size); +#if ESPRADIO_OSI_DEBUG + printf("osi: zalloc_internal %zu -> %p (caller=%p)\n", size, ret, __builtin_return_address(0)); +#endif + return ret; } static void * espradio_wifi_malloc(size_t size) { espradio_alloc_count++; + void *ret = espradio_arena_alloc(size); #if ESPRADIO_OSI_DEBUG - printf("osi: wifi_malloc %zu\n", size); + printf("osi: wifi_malloc %zu -> %p\n", size, ret); #endif - return espradio_arena_alloc(size); + return ret; } static void * espradio_wifi_realloc(void *ptr, size_t size) { @@ -1060,19 +1117,25 @@ static void * espradio_wifi_realloc(void *ptr, size_t size) { } static void * espradio_wifi_calloc(size_t n, size_t size) { + espradio_alloc_count++; + void *ret = espradio_arena_calloc(n, size); #if ESPRADIO_OSI_DEBUG - printf("osi: wifi_calloc n=%zu size=%zu\n", n, size); + printf("osi: wifi_calloc n=%zu size=%zu -> %p\n", n, size, ret); #endif - espradio_alloc_count++; - return espradio_arena_calloc(n, size); + return ret; } static void * espradio_wifi_zalloc(size_t size) { #if ESPRADIO_OSI_DEBUG - printf("osi: wifi_zalloc %zu\n", size); + printf("osi: wifi_zalloc %zu (caller=%p)", size, __builtin_return_address(0)); #endif espradio_alloc_count++; - return espradio_arena_calloc(1, size); + void *ret = espradio_arena_calloc(1, size); +#if ESPRADIO_OSI_DEBUG + printf(" -> %p\n", ret); + fflush(stdout); +#endif + return ret; } void espradio_arena_stats(uint32_t *used, uint32_t *capacity); @@ -1343,9 +1406,60 @@ void espradio_coex_adapter_init(void) { esp_err_t r = esp_coex_adapter_register(&g_coex_adapter_funcs); #if ESPRADIO_OSI_DEBUG printf("osi: esp_coex_adapter_register -> %ld\n", (long)r); + /* Dump adapter table to verify struct layout */ + { + uint32_t *p = (uint32_t *)&g_coex_adapter_funcs; + int n = sizeof(g_coex_adapter_funcs) / sizeof(uint32_t); + printf("osi: coex_adapter_funcs @ %p (%d words):\n", (void*)p, n); + for (int i = 0; i < n && i < 24; i++) { + printf(" [%2d] offset %3d = 0x%08lx\n", i, i*4, (unsigned long)p[i]); + } + } +#endif +} + + +/* Debug wrappers for wifi OSI semphr/queue functions */ +static void *espradio_dbg_wifi_thread_semphr_get(void) { + void *ret = espradio_wifi_thread_semphr_get(); +#if ESPRADIO_OSI_DEBUG + printf("osi: wifi_thread_semphr_get -> %p\n", ret); +#endif + return ret; +} + +static int32_t espradio_dbg_semphr_take(void *semphr, uint32_t block_time_tick) { +#if ESPRADIO_OSI_DEBUG + printf("osi: semphr_take sem=%p block=%lu\n", semphr, (unsigned long)block_time_tick); +#endif + int32_t ret = espradio_semphr_take(semphr, block_time_tick); +#if ESPRADIO_OSI_DEBUG + printf("osi: semphr_take -> %ld\n", (long)ret); +#endif + return ret; +} + +static int32_t espradio_dbg_semphr_give(void *semphr) { +#if ESPRADIO_OSI_DEBUG + printf("osi: semphr_give sem=%p\n", semphr); #endif + int32_t ret = espradio_semphr_give(semphr); +#if ESPRADIO_OSI_DEBUG + printf("osi: semphr_give -> %ld\n", (long)ret); +#endif + return ret; } +static int32_t espradio_dbg_queue_recv(void *ptr, void *item, uint32_t block_time_tick) { +#if ESPRADIO_OSI_DEBUG + printf("osi: queue_recv q=%p block=%lu\n", ptr, (unsigned long)block_time_tick); +#endif + int32_t ret = espradio_queue_recv(ptr, item, block_time_tick); +#if ESPRADIO_OSI_DEBUG + printf("osi: queue_recv -> %ld\n", (long)ret); +#endif + return ret; +} wifi_osi_funcs_t espradio_osi_funcs = { ._version = ESP_WIFI_OS_ADAPTER_VERSION, @@ -1363,9 +1477,9 @@ wifi_osi_funcs_t espradio_osi_funcs = { ._task_yield_from_isr = espradio_task_yield_from_isr, ._semphr_create = espradio_semphr_create, ._semphr_delete = espradio_semphr_delete, - ._semphr_take = espradio_semphr_take, - ._semphr_give = espradio_semphr_give, - ._wifi_thread_semphr_get = espradio_wifi_thread_semphr_get, + ._semphr_take = espradio_dbg_semphr_take, + ._semphr_give = espradio_dbg_semphr_give, + ._wifi_thread_semphr_get = espradio_dbg_wifi_thread_semphr_get, ._mutex_create = espradio_mutex_create, ._recursive_mutex_create = espradio_recursive_mutex_create, ._mutex_delete = espradio_mutex_delete, @@ -1377,7 +1491,7 @@ wifi_osi_funcs_t espradio_osi_funcs = { ._queue_send_from_isr = espradio_queue_send_from_isr, ._queue_send_to_back = espradio_queue_send_to_back, ._queue_send_to_front = espradio_queue_send_to_front, - ._queue_recv = espradio_queue_recv, + ._queue_recv = espradio_dbg_queue_recv, ._queue_msg_waiting = espradio_queue_msg_waiting, ._event_group_create = espradio_event_group_create, ._event_group_delete = espradio_event_group_delete, diff --git a/radio.c b/radio.c index 0b01c67..d6ac3c5 100644 --- a/radio.c +++ b/radio.c @@ -2,6 +2,7 @@ #include "espradio.h" #include "soc/interrupts.h" #include +#include #ifndef ESPRADIO_RADIO_DEBUG #define ESPRADIO_RADIO_DEBUG 0 @@ -39,8 +40,6 @@ extern char gChmCxt[252]; /* libnet80211.a: channel manager contex extern void phy_get_romfunc_addr(void); extern void *g_phyFuns; extern wifi_osi_funcs_t *g_osi_funcs_p; -extern int rtc_get_reset_reason(int cpu_no); - /* Stub for the WIFI_INIT_CONFIG_DEFAULT() macro; also kept filled as fallback. */ wifi_osi_funcs_t g_wifi_osi_funcs = {0}; @@ -119,12 +118,121 @@ void espradio_rom_hooks_init(void) { ets_install_lock(ets_intr_lock, ets_intr_unlock); } +/* Disable ALL watchdog timers so that if the CPU hangs or faults, we get + * an exception vector (captured in RTC STORE) instead of a Super WDT + * full-SoC reset that clears the RTC domain. Each WDT requires unlocking + * its write-protect register first. + * + * Also disable the clock glitch detector and power glitch detector. + * The WiFi radio TX causes EMI/power noise that trips these detectors, + * resulting in a GLITCH_RTC_RESET (reset_reason=19) that wipes the + * entire RTC domain. ESP-IDF disables these during normal startup. */ +static void espradio_disable_all_wdt(void) { +#ifdef __XTENSA__ + /* --- Clock glitch detector --- */ + /* RTC_CNTL_ANA_CONF_REG = 0x60008034, bit 20 = GLITCH_RST_EN */ + *(volatile uint32_t *)0x60008034 &= ~(1u << 20); /* GLITCH_RST_EN=0 */ + + /* --- FIB_SEL: override eFuse with register values --- */ + /* RTC_CNTL_FIB_SEL_REG = 0x60008148, bits[2:0]: + * When bit=1, eFuse value overrides register → can't disable via register + * When bit=0, register value is used → our GLITCH_RST_EN=0 takes effect + * Default=0x7 (all eFuse), ESP-IDF sets 0x1. Set 0x0 to force all to register. */ + *(volatile uint32_t *)0x60008148 = 0x0; + + /* --- Power glitch detector --- */ + /* RTC_CNTL_PG_CTRL_REG = 0x60008144, bit 31 = POWER_GLITCH_EN */ + *(volatile uint32_t *)0x60008144 &= ~(1u << 31); /* POWER_GLITCH_EN=0 */ + + /* --- Brownout detector --- */ + /* RTC_CNTL_BROWN_OUT_REG = 0x600080E8, bit 30 = ENA, bit 26 = RST_ENA */ + *(volatile uint32_t *)0x600080E8 &= ~((1u << 30) | (1u << 26)); /* disable BOD + BOD reset */ + + /* --- Super WDT (SWD) --- */ + /* RTC_CNTL_SWD_WPROTECT_REG = 0x600080B8, key = 0x8F1D312A */ + /* RTC_CNTL_SWD_CONF_REG = 0x600080B4, bit 30 = SWD_DISABLE */ + *(volatile uint32_t *)0x600080B8 = 0x8F1D312A; /* unlock */ + *(volatile uint32_t *)0x600080B4 |= (1u << 30); /* SWD_DISABLE=1 */ + *(volatile uint32_t *)0x600080B8 = 0; /* re-lock */ + + /* --- RTC WDT --- */ + /* RTC_CNTL_WDTWPROTECT_REG = 0x600080B0, key = 0x50D83AA1 */ + /* RTC_CNTL_WDTCONFIG0_REG = 0x60008098, bit 31 = WDT_EN */ + *(volatile uint32_t *)0x600080B0 = 0x50D83AA1; /* unlock */ + *(volatile uint32_t *)0x60008098 &= ~(1u << 31); /* WDT_EN=0 */ + *(volatile uint32_t *)0x600080B0 = 0; /* re-lock */ + + /* --- Timer Group 0 MWDT --- */ + /* TIMG_WDTWPROTECT_REG(0) = 0x6001F064, key = 0x50D83AA1 */ + /* TIMG_WDTCONFIG0_REG(0) = 0x6001F048, bit 31 = WDT_EN */ + *(volatile uint32_t *)0x6001F064 = 0x50D83AA1; /* unlock */ + *(volatile uint32_t *)0x6001F048 &= ~(1u << 31); /* WDT_EN=0 */ + *(volatile uint32_t *)0x6001F064 = 0; /* re-lock */ + + /* --- Timer Group 1 MWDT --- */ + /* TIMG_WDTWPROTECT_REG(1) = 0x60020064, key = 0x50D83AA1 */ + /* TIMG_WDTCONFIG0_REG(1) = 0x60020048, bit 31 = WDT_EN */ + *(volatile uint32_t *)0x60020064 = 0x50D83AA1; /* unlock */ + *(volatile uint32_t *)0x60020048 &= ~(1u << 31); /* WDT_EN=0 */ + *(volatile uint32_t *)0x60020064 = 0; /* re-lock */ + +#else /* RISC-V (ESP32-C3) — different RTC_CNTL offsets, same TIMG offsets */ + + /* --- Clock glitch detector --- */ + *(volatile uint32_t *)0x60008034 &= ~(1u << 20); /* GLITCH_RST_EN=0 */ + /* RTC_CNTL_FIB_SEL_REG = 0x6000810C */ + *(volatile uint32_t *)0x6000810C = 0x0; /* force register values */ + + /* --- Power glitch detector --- */ + /* RTC_CNTL_PG_CTRL_REG = 0x60008124 */ + *(volatile uint32_t *)0x60008124 &= ~(1u << 31); /* POWER_GLITCH_EN=0 */ + + /* --- Brownout detector --- */ + /* RTC_CNTL_BROWN_OUT_REG = 0x600080D8 */ + *(volatile uint32_t *)0x600080D8 &= ~((1u << 30) | (1u << 26)); + + /* --- Super WDT (SWD) --- */ + /* RTC_CNTL_SWD_WPROTECT_REG = 0x600080B0, key = 0x8F1D312A */ + /* RTC_CNTL_SWD_CONF_REG = 0x600080AC, bit 30 = SWD_DISABLE */ + *(volatile uint32_t *)0x600080B0 = 0x8F1D312A; /* unlock */ + *(volatile uint32_t *)0x600080AC |= (1u << 30); /* SWD_DISABLE=1 */ + *(volatile uint32_t *)0x600080B0 = 0; /* re-lock */ + + /* --- RTC WDT --- */ + /* RTC_CNTL_WDTWPROTECT_REG = 0x600080A8, key = 0x50D83AA1 */ + /* RTC_CNTL_WDTCONFIG0_REG = 0x60008090, bit 31 = WDT_EN */ + *(volatile uint32_t *)0x600080A8 = 0x50D83AA1; /* unlock */ + *(volatile uint32_t *)0x60008090 &= ~(1u << 31); /* WDT_EN=0 */ + *(volatile uint32_t *)0x600080A8 = 0; /* re-lock */ + + /* --- Timer Group 0 MWDT --- */ + *(volatile uint32_t *)0x6001F064 = 0x50D83AA1; /* unlock */ + *(volatile uint32_t *)0x6001F048 &= ~(1u << 31); /* WDT_EN=0 */ + *(volatile uint32_t *)0x6001F064 = 0; /* re-lock */ + + /* --- Timer Group 1 MWDT --- */ + *(volatile uint32_t *)0x60020064 = 0x50D83AA1; /* unlock */ + *(volatile uint32_t *)0x60020048 &= ~(1u << 31); /* WDT_EN=0 */ + *(volatile uint32_t *)0x60020064 = 0; /* re-lock */ + +#endif +} + esp_err_t espradio_wifi_init(void) { espradio_rom_hooks_init(); - RADIO_DBG("espradio: early_reset_reason cpu0=%d cpu1=%d\n", - rtc_get_reset_reason(0), rtc_get_reset_reason(1)); + + espradio_disable_all_wdt(); + + RADIO_DBG("espradio: before esp_wifi_bt_power_domain_on\n"); + esp_wifi_bt_power_domain_on(); + RADIO_DBG("espradio: after esp_wifi_bt_power_domain_on\n"); + +#ifndef __XTENSA__ + /* C3 (RISC-V) needs phy_get_romfunc_addr called early. + * On S3 (Xtensa) it is called internally by register_chipv7_phy. */ phy_get_romfunc_addr(); RADIO_DBG("espradio: phy_get_romfunc_addr g_phyFuns=%p\n", g_phyFuns); +#endif /* Allocate the OSI table on the heap, with 256-byte alignment and a * 256-byte guard region on each side. PMP analysis proved that the @@ -146,24 +254,79 @@ esp_err_t espradio_wifi_init(void) { * checks g_osi_funcs_p at entry and skips critical init if it's non-zero. */ memcpy(&g_wifi_osi_funcs, &espradio_osi_funcs, sizeof(wifi_osi_funcs_t)); +#ifdef __XTENSA__ + /* Clang for Xtensa has an OR-offset bug: &struct->field computes + * (struct_base | field_offset) instead of (struct_base + field_offset). + * For wpa_crypto_funcs at offset 4, this fires when bit 2 of struct_base + * is set — e.g. cfg at 0x3fc9c59c has bit 2 = 1, so &cfg.wpa_crypto_funcs + * == cfg_base (not cfg_base+4), causing memcpy to overwrite cfg.osi_funcs + * with size=44 and cfg.wpa_crypto_funcs.size with version=1. + * + * Two-layer fix: + * 1. static + aligned(8) guarantees cfg_base bits 0-2 are always 0, + * so the OR equals the ADD for any offset. + * 2. Use offsetof + char* arithmetic for the wpa_crypto_funcs pointer + * so the compiler emits ADD rather than OR regardless of address. */ + static wifi_init_config_t cfg __attribute__((aligned(8))); + memset(&cfg, 0, sizeof(cfg)); + cfg.osi_funcs = s_heap_osi_funcs; + cfg.static_rx_buf_num = 10; + cfg.dynamic_rx_buf_num = 32; + cfg.tx_buf_type = 1; + cfg.static_tx_buf_num = 0; + cfg.dynamic_tx_buf_num = 32; + cfg.rx_mgmt_buf_type = 0; + cfg.rx_mgmt_buf_num = 5; + cfg.cache_tx_buf_num = 0; + cfg.csi_enable = 0; + cfg.ampdu_rx_enable = 0; + cfg.ampdu_tx_enable = 0; + cfg.amsdu_tx_enable = 0; + cfg.nvs_enable = 0; + cfg.nano_enable = 0; + cfg.rx_ba_win = 6; + cfg.wifi_task_core_id = 0; + cfg.beacon_max_len = 752; + cfg.mgmt_sbuf_num = 32; + cfg.feature_caps = 0; + cfg.sta_disconnected_pm = false; + cfg.espnow_max_encrypt_num = 7; + cfg.tx_hetb_queue_num = 1; + cfg.dump_hesigb_enable = false; + cfg.magic = 0x1F2F3F4F; + /* Copy wpa_crypto_funcs using offsetof+char* so the compiler emits ADD + * not OR for the destination address (see aligned(8) comment above). */ + { + const wpa_crypto_funcs_t *src = &g_wifi_default_wpa_crypto_funcs; + wpa_crypto_funcs_t *dst = (wpa_crypto_funcs_t *)( + (char *)&cfg + offsetof(wifi_init_config_t, wpa_crypto_funcs)); + memcpy(dst, src, sizeof(*dst)); + } +#else + /* C3 (RISC-V): no OR-offset bug, use the macro directly. */ wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); cfg.osi_funcs = s_heap_osi_funcs; cfg.nvs_enable = 0; +#endif extern wifi_osi_funcs_t *wifi_funcs; wifi_funcs = s_heap_osi_funcs; - RADIO_DBG("espradio: before esp_wifi_bt_power_domain_on\n"); - esp_wifi_bt_power_domain_on(); - RADIO_DBG("espradio: after esp_wifi_bt_power_domain_on\n"); espradio_bt_irq_prewire(); + RADIO_DBG("espradio: after bt_irq_prewire\n"); extern void espradio_coex_adapter_init(void); espradio_coex_adapter_init(); + RADIO_DBG("espradio: after coex_adapter_init\n"); extern esp_err_t coex_pre_init(void); - coex_pre_init(); + RADIO_DBG("espradio: calling coex_pre_init\n"); + esp_err_t coex_rc = coex_pre_init(); + RADIO_DBG("espradio: coex_pre_init returned %d\n", (int)coex_rc); + RADIO_DBG("espradio: before esp_wifi_init_internal cfg.osi_funcs=%p\n", (void*)cfg.osi_funcs); esp_err_t ret = esp_wifi_init_internal(&cfg); + RADIO_DBG("espradio: esp_wifi_init_internal returned %d\n", (int)ret); + if (ret == 0) { extern void esp_phy_modem_init(void); esp_phy_modem_init(); diff --git a/radio.go b/radio.go index 3eed364..d44b0af 100644 --- a/radio.go +++ b/radio.go @@ -6,6 +6,7 @@ package espradio #cgo CFLAGS: -Iblobs/headers #cgo CFLAGS: -DCONFIG_SOC_WIFI_NAN_SUPPORT=0 #cgo CFLAGS: -DESPRADIO_PHY_PATCH_ROMFUNCS=0 +#cgo CFLAGS: -fno-short-enums #include "espradio.h" */ @@ -97,11 +98,32 @@ func startSchedTicker() { case <-isrKick: } schedOnce() + runtime.Gosched() } }() } +var wifiInitDone uint32 + func schedOnce() { + // Mask WiFi CPU interrupt before the ISR softcall. On Xtensa (ESP32-S3) + // the WiFi interrupt is level-triggered at level 1. If the MAC asserts + // its interrupt while we're already iterating the ISR handlers below, + // the hardware ISR preempts us and re-entrantly calls the blob's ISR + // handler, corrupting its state and crashing. Masking first prevents + // this; espradio_wifi_unmask() at the end re-enables the interrupt. + C.espradio_ints_off(C.uint32_t(1 << wifiCPUInterrupt)) + + // Restore ROM pointers BEFORE any blob code runs. The blob reads + // pTxRx, pp_wdev_funcs etc. during ISR/queue/timer processing below. + C.espradio_restore_rom_ptrs() + + // Poll WiFi ISR: work around missing hardware interrupt on ESP32-S3. + // Only poll after init is complete (blob ISR not registered until then). + if atomic.LoadUint32(&wifiInitDone) != 0 { + C.espradio_call_wifi_isr() + } + for C.espradio_isr_ring_tail() != C.espradio_isr_ring_head() { idx := C.espradio_isr_ring_tail() q := C.espradio_isr_ring_entry_queue(idx) @@ -124,6 +146,10 @@ func schedOnce() { } } + // Restore critical ROM pointer variables that WiFi DMA may have + // corrupted (pTxRx, our_tx_eb, our_wait_eb, lmacConfMib_ptr). + C.espradio_restore_rom_ptrs() + C.espradio_wifi_unmask() } @@ -134,17 +160,24 @@ func kickSched() { } } +// arenaPool keeps the arena backing memory reachable from Go so the GC +// won't collect it. The WiFi blob stores pointers into this pool in ROM +// BSS (outside the GC's scan range), so individual malloc'd objects would +// be collected. One large pool kept alive by this global is safe. +var arenaPool []byte + // Enable and configure the radio. func Enable(config Config) error { + // Allocate arena pool from Go heap and hand it to C. + arenaPool = make([]byte, arenaPoolSize) + C.espradio_arena_init((*C.uint8_t)(unsafe.Pointer(&arenaPool[0])), C.size_t(arenaPoolSize)) + startSchedTicker() time.Sleep(schedTickerMs * time.Millisecond) initHardware() C.espradio_ensure_osi_ptr() - wifiISR = interrupt.New(1, func(interrupt.Interrupt) { - C.espradio_call_wifi_isr() - kickSched() - }) + wifiISR = interrupt.New(wifiCPUInterrupt, wifiISRHandler) wifiISR.Enable() C.espradio_wifi_int_raise_priority() @@ -163,6 +196,7 @@ func Enable(config Config) error { } C.espradio_wifi_init_completed() C.espradio_wifi_int_to_level() + atomic.StoreUint32(&wifiInitDone, 1) schedOnce() C.espradio_netif_init_netstack_cb() @@ -182,13 +216,39 @@ func Start() error { } } + C.espradio_set_country_eu_manual() + if code := C.espradio_esp_wifi_start(); code != C.ESP_OK { return makeError(code) } + // Disable modem-sleep power management. The blob's default + // WIFI_PS_MIN_MODEM fires PM timer callbacks (pm_dream, pm_go_to_wake, + // etc.) that call ppCheckTxConnTrafficIdle. Under cooperative scheduling + // the TX frame queues may be in an inconsistent state when those PM + // callbacks run, causing NULL-pointer crashes in ppCheckIsConnTraffic. + C.esp_wifi_set_ps(C.WIFI_PS_NONE) + + // The blob's esp_wifi_start posts a START command to ppTask's queue + // and returns. espradio_esp_wifi_start already called post_start_cb + // which relocates heap tables. Now pump the scheduler so ppTask + // processes START (calls pp_attach, ppInitTxq, etc.) and the critical + // ROM pointer variables (pTxRx, our_tx_eb, …) get initialised. + for i := 0; i < 40; i++ { + schedOnce() + runtime.Gosched() + } + // Snapshot the ROM pointers now that they are valid. + C.espradio_save_rom_ptrs() + return nil } +// DebugISRCount returns the number of WiFi ISR invocations (for debugging). +func DebugISRCount() uint32 { + return uint32(C.espradio_get_wifi_isr_count()) +} + // Scan performs a single Wi-Fi scan pass and returns the list of discovered access points. func Scan() ([]AccessPoint, error) { C.espradio_ensure_osi_ptr() @@ -269,6 +329,13 @@ func Connect(cfg STAConfig) error { select { case res := <-connectResult: if res.Connected { + // The blob fires WIFI_EVENT_STA_CONNECTED before its internal + // TX path (ppCheckIsConnTraffic) is fully initialized. Pump the + // scheduler to let the blob finish setup before callers try to TX. + for i := 0; i < 20; i++ { + schedOnce() + time.Sleep(10 * time.Millisecond) + } return nil } return makeError(C.esp_err_t(res.Reason)) @@ -281,6 +348,7 @@ func Connect(cfg STAConfig) error { func espradio_on_wifi_event(eventID int32, data unsafe.Pointer) { switch eventID { case C.WIFI_EVENT_STA_CONNECTED: + C.espradio_netif_set_connected(1) ev := (*C.wifi_event_sta_connected_t)(data) ssidLen := int(ev.ssid_len) if ssidLen > 32 { @@ -298,6 +366,7 @@ func espradio_on_wifi_event(eventID int32, data unsafe.Pointer) { } case C.WIFI_EVENT_STA_DISCONNECTED: + C.espradio_netif_set_connected(0) ev := (*C.wifi_event_sta_disconnected_t)(data) connectMu.Lock() ch := connectResult @@ -791,6 +860,7 @@ func espradio_semphr_take(semphr unsafe.Pointer, block_time_tick uint32) int32 { timeout = time.Duration(block_time_tick) * time.Millisecond } + iters := 0 for { if semTryTake(sem) { return 1 @@ -798,6 +868,7 @@ func espradio_semphr_take(semphr unsafe.Pointer, block_time_tick uint32) int32 { if !forever && time.Since(start) >= timeout { return 0 } + iters++ safeGosched() } } diff --git a/radio_esp32c3.go b/radio_esp32c3.go index 02df309..4cd8d7f 100644 --- a/radio_esp32c3.go +++ b/radio_esp32c3.go @@ -17,12 +17,16 @@ import "C" import ( "device/esp" + "runtime/interrupt" _ "tinygo.org/x/espradio/esp32c3" ) // ─── Hardware init ─────────────────────────────────────────────────────────── +// CPU interrupt number for WiFi MAC. On RISC-V, interrupt 1 is valid. +const wifiCPUInterrupt = 1 + func initHardware() error { // See: // https://github.com/esp-rs/esp-wifi/blob/main/esp-wifi/src/common_adapter/common_adapter_esp32c3.rs#L18 @@ -58,3 +62,14 @@ func initHardware() error { // This is the value used for the ESP32-C3, see: // https://github.com/esp-rs/esp-wifi/blob/v0.2.0/esp-wifi/src/timer/riscv.rs#L28 const ticksPerSecond = 16_000_000 + +// C3 has only 321KB DRAM total; keep the arena pool small. +const arenaPoolSize = 32 * 1024 + +// ESP32-C3 (RISC-V): call the blob's WiFi ISR directly from the +// hardware interrupt handler. On RISC-V the interrupt context can +// safely call the blob's ISR without stack overflow concerns. +func wifiISRHandler(interrupt.Interrupt) { + C.espradio_call_wifi_isr() + kickSched() +} diff --git a/radio_esp32c3_qemu.go b/radio_esp32c3_qemu.go index ca4a9cd..b5e980b 100644 --- a/radio_esp32c3_qemu.go +++ b/radio_esp32c3_qemu.go @@ -12,8 +12,19 @@ package espradio */ import "C" +import "runtime/interrupt" + // ticksPerSecond matches the real ESP32-C3 value used to convert timer ticks. const ticksPerSecond = 16_000_000 +// wifiCPUInterrupt is the CPU interrupt number for WiFi MAC (matches real C3). +const wifiCPUInterrupt = 1 + +// arenaPoolSize matches the real C3 value. +const arenaPoolSize = 32 * 1024 + // initHardware is a no-op for QEMU: no modem power / clock hardware present. func initHardware() error { return nil } + +// wifiISRHandler is a no-op for QEMU: no real WiFi interrupts. +func wifiISRHandler(interrupt.Interrupt) {} diff --git a/radio_esp32s3.go b/radio_esp32s3.go new file mode 100644 index 0000000..fbea6c8 --- /dev/null +++ b/radio_esp32s3.go @@ -0,0 +1,78 @@ +//go:build esp32s3 + +package espradio + +/* +#cgo CFLAGS: -Iblobs/include +#cgo CFLAGS: -Iblobs/include/esp32s3 +#cgo CFLAGS: -Iblobs/include/local +#cgo CFLAGS: -Iblobs/headers +#cgo CFLAGS: -DCONFIG_SOC_WIFI_NAN_SUPPORT=0 +#cgo CFLAGS: -DESPRADIO_PHY_PATCH_ROMFUNCS=0 +#cgo CFLAGS: -fno-short-enums +#cgo LDFLAGS: -Lblobs/libs/esp32s3 -lcoexist -lcore -lmesh -lnet80211 -lespnow -lregulatory -lphy -lpp -lwpa_supplicant + +#include "include.h" +*/ +import "C" + +import ( + "device/esp" + "runtime/interrupt" + + _ "tinygo.org/x/espradio/esp32s3" +) + +// ─── Hardware init ─────────────────────────────────────────────────────────── + +// CPU interrupt number for WiFi MAC. On Xtensa, TinyGo dispatches only lines +// 6-30. Interrupt 12 is a level-triggered, level-1 interrupt in the allocatable range. +const wifiCPUInterrupt = 12 + +func initHardware() error { + // See: + // https://github.com/esp-rs/esp-hal/blob/main/esp-radio/src/radio_clocks/clocks_ll/esp32s3.rs + + const ( + SYSTEM_WIFIBB_RST = 1 << 0 + SYSTEM_FE_RST = 1 << 1 + SYSTEM_WIFIMAC_RST = 1 << 2 + SYSTEM_BTBB_RST = 1 << 3 // Bluetooth Baseband + SYSTEM_BTMAC_RST = 1 << 4 // deprecated + SYSTEM_RW_BTMAC_RST = 1 << 9 // Bluetooth MAC + SYSTEM_RW_BTMAC_REG_RST = 1 << 11 // Bluetooth MAC Registers + SYSTEM_BTBB_REG_RST = 1 << 13 // Bluetooth Baseband Registers + ) + + const MODEM_RESET_FIELD_WHEN_PU = SYSTEM_WIFIBB_RST | + SYSTEM_FE_RST | + SYSTEM_WIFIMAC_RST | + SYSTEM_BTBB_RST | + SYSTEM_BTMAC_RST | + SYSTEM_RW_BTMAC_RST | + SYSTEM_RW_BTMAC_REG_RST | + SYSTEM_BTBB_REG_RST + + esp.RTC_CNTL.DIG_PWC.ClearBits(1 << 17) // WIFI_FORCE_PD + esp.APB_CTRL.WIFI_RST_EN.SetBits(MODEM_RESET_FIELD_WHEN_PU) + esp.APB_CTRL.WIFI_RST_EN.ClearBits(MODEM_RESET_FIELD_WHEN_PU) + esp.RTC_CNTL.DIG_ISO.ClearBits(1 << 28) // WIFI_FORCE_ISO + + return nil +} + +// ESP32-S3 uses the Xtensa systimer at 16 MHz. +// See: https://github.com/esp-rs/esp-hal/blob/main/esp-hal/src/timer/systimer.rs +const ticksPerSecond = 16_000_000 + +// S3 has 416KB SRAM1; larger arena pool for blob allocations. +const arenaPoolSize = 80 * 1024 + +// ESP32-S3 (Xtensa): don't run the blob ISR from interrupt context — the deep +// windowed call chains can overflow the interrupted goroutine's 8KB stack. +// Just mask the level-triggered interrupt and wake the scheduler; schedOnce() +// will call espradio_call_wifi_isr() on its own goroutine stack. +func wifiISRHandler(interrupt.Interrupt) { + C.espradio_ints_off(C.uint32_t(1 << wifiCPUInterrupt)) + kickSched() +} diff --git a/tools/patch_xtensa_literals.go b/tools/patch_xtensa_literals.go new file mode 100644 index 0000000..5a5f2d4 --- /dev/null +++ b/tools/patch_xtensa_literals.go @@ -0,0 +1,501 @@ +// Fix Xtensa relocations for LLD compatibility. +// +// Two issues are addressed: +// +// 1. LLD ignores R_XTENSA_ASM_EXPAND relocations, which carry the correct +// addend for call targets to static (file-scope) functions. The paired +// R_XTENSA_32 relocation on the literal pool entry has addend=0, causing +// LLD to fill the literal with the section start instead of the function +// entry point. This tool propagates the addend from R_XTENSA_ASM_EXPAND +// to the corresponding R_XTENSA_32 literal pool entry. +// +// 2. The Xtensa GCC assembler stores addends in-place in section data even +// for RELA relocations (where r_addend should carry the addend). LLD +// follows standard RELA convention and ignores in-place values, causing +// jump tables and other section-relative R_XTENSA_32 relocations to +// resolve to the section base instead of the correct target. This tool +// moves in-place addends into the RELA r_addend field. +// +// Usage: +// +// go run fix_xtensa_literals.go [-n] blobs/libs/esp32s3/*.a +// +//go:build ignore + +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// ELF constants +const ( + etREL = 1 + emXtensa = 94 + shtRELA = 4 + shtSYMTAB = 2 + sttSECTION = 3 +) + +// Xtensa relocation types +const ( + rXtensa32 = 1 + rXtensaASMExpand = 11 + rXtensaSLOT0OP = 20 +) + +func readU16(data []byte, off int) uint16 { + return binary.LittleEndian.Uint16(data[off:]) +} + +func readU32(data []byte, off int) uint32 { + return binary.LittleEndian.Uint32(data[off:]) +} + +func readI32(data []byte, off int) int32 { + return int32(binary.LittleEndian.Uint32(data[off:])) +} + +func writeI32(data []byte, off int, val int32) { + binary.LittleEndian.PutUint32(data[off:], uint32(val)) +} + +func getString(data []byte, strtabOff, idx int) string { + start := strtabOff + idx + end := bytes.IndexByte(data[start:], 0) + if end < 0 { + return "" + } + return string(data[start : start+end]) +} + +type section struct { + nameIdx uint32 + typ uint32 + flags uint32 + addr uint32 + offset uint32 + size uint32 + link uint32 + info uint32 + addralign uint32 + entsize uint32 + index int + name string +} + +type relaEntry struct { + fileOff int + rOffset uint32 + rType uint8 + rSym uint32 + rAddend int32 + index int +} + +func fixObject(data []byte) ([]byte, int) { + data = append([]byte(nil), data...) // copy + + if len(data) < 52 || string(data[0:4]) != "\x7fELF" { + return data, 0 + } + if data[4] != 1 { // ELF32 + return data, 0 + } + if readU16(data, 16) != etREL || readU16(data, 18) != emXtensa { + return data, 0 + } + + shOff := int(readU32(data, 32)) + shEntSize := int(readU16(data, 46)) + shNum := int(readU16(data, 48)) + shStrNdx := int(readU16(data, 50)) + + sections := make([]section, shNum) + for i := 0; i < shNum; i++ { + off := shOff + i*shEntSize + sections[i] = section{ + nameIdx: readU32(data, off), + typ: readU32(data, off+4), + flags: readU32(data, off+8), + addr: readU32(data, off+12), + offset: readU32(data, off+16), + size: readU32(data, off+20), + link: readU32(data, off+24), + info: readU32(data, off+28), + addralign: readU32(data, off+32), + entsize: readU32(data, off+36), + index: i, + } + } + + // Section name string table + shstrtabOff := int(sections[shStrNdx].offset) + for i := range sections { + sections[i].name = getString(data, shstrtabOff, int(sections[i].nameIdx)) + } + + totalFixes := 0 + + // --- Pass 1: Propagate ASM_EXPAND addends to literal pool R_XTENSA_32 --- + + symSecIdx := func(symtabOff, symtabEntSize int, symIdx uint32) uint16 { + off := symtabOff + int(symIdx)*symtabEntSize + return readU16(data, off+14) + } + + for _, relaSec := range sections { + if relaSec.typ != shtRELA { + continue + } + + relaOff := int(relaSec.offset) + relaSize := int(relaSec.size) + relaEntSize := int(relaSec.entsize) + if relaEntSize == 0 { + relaEntSize = 12 + } + numEntries := relaSize / relaEntSize + + symtabSec := sections[relaSec.link] + symtabOff := int(symtabSec.offset) + symtabEntSize := int(symtabSec.entsize) + if symtabEntSize == 0 { + symtabEntSize = 16 + } + + entries := make([]relaEntry, numEntries) + for i := 0; i < numEntries; i++ { + entOff := relaOff + i*relaEntSize + rInfo := readU32(data, entOff+4) + entries[i] = relaEntry{ + fileOff: entOff, + rOffset: readU32(data, entOff), + rType: uint8(rInfo & 0xFF), + rSym: rInfo >> 8, + rAddend: readI32(data, entOff+8), + index: i, + } + } + + // Index: offset -> entries at that offset + byOffset := map[uint32][]int{} + for i, e := range entries { + byOffset[e.rOffset] = append(byOffset[e.rOffset], i) + } + + // Index: offset -> R_XTENSA_32 entries + r32ByOffset := map[uint32][]int{} + for i, e := range entries { + if e.rType == rXtensa32 { + r32ByOffset[e.rOffset] = append(r32ByOffset[e.rOffset], i) + } + } + + for _, e := range entries { + if e.rType != rXtensaASMExpand || e.rAddend == 0 { + continue + } + + asmTargetSec := symSecIdx(symtabOff, symtabEntSize, e.rSym) + if int(asmTargetSec) == 0 || int(asmTargetSec) >= len(sections) { + continue + } + if !strings.HasPrefix(sections[asmTargetSec].name, ".text.") { + continue + } + + // Find SLOT0_OP at the same offset + var slot0 *relaEntry + for _, idx := range byOffset[e.rOffset] { + if entries[idx].rType == rXtensaSLOT0OP { + slot0 = &entries[idx] + break + } + } + if slot0 == nil { + continue + } + + literalOffset := uint32(slot0.rAddend) + + // Find R_XTENSA_32 at the literal offset targeting same section + for _, idx := range r32ByOffset[literalOffset] { + r32 := &entries[idx] + r32Target := symSecIdx(symtabOff, symtabEntSize, r32.rSym) + if r32Target == asmTargetSec && r32.rAddend == 0 { + writeI32(data, r32.fileOff+8, e.rAddend) + r32.rAddend = e.rAddend + + // Clear in-place literal value + litSecIdx := int(relaSec.info) + litSec := sections[litSecIdx] + inplaceOff := int(litSec.offset) + int(r32.rOffset) + if int(r32.rOffset)+4 <= int(litSec.size) { + writeI32(data, inplaceOff, 0) + } + totalFixes++ + break + } + } + } + } + + // --- Pass 2: Move in-place addends into RELA r_addend for R_XTENSA_32 --- + + for _, relaSec := range sections { + if relaSec.typ != shtRELA { + continue + } + + relaOff := int(relaSec.offset) + relaSize := int(relaSec.size) + relaEntSize := int(relaSec.entsize) + if relaEntSize == 0 { + relaEntSize = 12 + } + numEntries := relaSize / relaEntSize + + targetSecIdx := int(relaSec.info) + if targetSecIdx == 0 || targetSecIdx >= len(sections) { + continue + } + targetSec := sections[targetSecIdx] + targetDataOff := int(targetSec.offset) + targetDataSize := int(targetSec.size) + + symtabSec := sections[relaSec.link] + symtabOff := int(symtabSec.offset) + symtabEntSize := int(symtabSec.entsize) + if symtabEntSize == 0 { + symtabEntSize = 16 + } + + for i := 0; i < numEntries; i++ { + entOff := relaOff + i*relaEntSize + rOffset := readU32(data, entOff) + rInfo := readU32(data, entOff+4) + rAddend := readI32(data, entOff+8) + rType := uint8(rInfo & 0xFF) + rSym := rInfo >> 8 + + if rType != rXtensa32 { + continue + } + + if int(rOffset)+4 > targetDataSize { + continue + } + + inplaceVal := readI32(data, targetDataOff+int(rOffset)) + if inplaceVal == 0 { + continue + } + + // Check if symbol is a section symbol (STT_SECTION) + symOff := symtabOff + int(rSym)*symtabEntSize + stInfo := data[symOff+12] + stType := stInfo & 0xf + if stType != sttSECTION { + continue + } + + newAddend := rAddend + inplaceVal + writeI32(data, entOff+8, newAddend) + writeI32(data, targetDataOff+int(rOffset), 0) + totalFixes++ + } + } + + return data, totalFixes +} + +// ar archive parsing + +func processArchive(archivePath string, dryRun bool) (int, error) { + f, err := os.Open(archivePath) + if err != nil { + return 0, err + } + defer f.Close() + + // Read the entire archive + archiveData, err := io.ReadAll(f) + if err != nil { + return 0, err + } + f.Close() + + if len(archiveData) < 8 || string(archiveData[:8]) != "!\n" { + return 0, fmt.Errorf("%s: not an ar archive", archivePath) + } + + type member struct { + name string + data []byte + } + + var members []member + totalFixes := 0 + modified := false + + pos := 8 + for pos < len(archiveData) { + if pos%2 == 1 { + pos++ // ar entries are 2-byte aligned + } + if pos+60 > len(archiveData) { + break + } + header := archiveData[pos : pos+60] + if string(header[58:60]) != "`\n" { + break + } + + nameField := strings.TrimRight(string(header[0:16]), " ") + sizeStr := strings.TrimRight(string(header[48:58]), " ") + var size int + fmt.Sscanf(sizeStr, "%d", &size) + + pos += 60 + if pos+size > len(archiveData) { + break + } + + memberData := archiveData[pos : pos+size] + pos += size + + // Resolve extended name (GNU ar uses /NNN for long names) + name := nameField + if strings.HasSuffix(name, "/") { + name = name[:len(name)-1] + } + + // Skip symbol table and string table pseudo-members + if name == "/" || name == "//" || name == "" { + members = append(members, member{name: nameField, data: memberData}) + continue + } + + fixedData, fixes := fixObject(memberData) + totalFixes += fixes + if fixes > 0 { + modified = true + members = append(members, member{name: nameField, data: fixedData}) + } else { + members = append(members, member{name: nameField, data: memberData}) + } + } + + if modified && !dryRun { + // Rewrite the archive + var buf bytes.Buffer + buf.WriteString("!\n") + + for _, m := range members { + if buf.Len()%2 == 1 { + buf.WriteByte('\n') + } + + // Reconstruct header + nameField := m.name + for len(nameField) < 16 { + nameField += " " + } + sizeStr := fmt.Sprintf("%-10d", len(m.data)) + + buf.WriteString(nameField) + buf.WriteString("0 ") // mtime (12 bytes) + buf.WriteString("0 ") // uid (6 bytes) + buf.WriteString("0 ") // gid (6 bytes) + buf.WriteString("100644 ") // mode (8 bytes) + buf.WriteString(sizeStr) + buf.WriteString("`\n") + buf.Write(m.data) + } + + if err := os.WriteFile(archivePath, buf.Bytes(), 0644); err != nil { + return totalFixes, fmt.Errorf("writing %s: %w", archivePath, err) + } + } + + return totalFixes, nil +} + +func processObject(path string, dryRun bool) (int, error) { + data, err := os.ReadFile(path) + if err != nil { + return 0, err + } + + fixedData, fixes := fixObject(data) + if fixes > 0 && !dryRun { + if err := os.WriteFile(path, fixedData, 0644); err != nil { + return fixes, err + } + } + return fixes, nil +} + +func main() { + args := os.Args[1:] + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "Usage: %s [-n] [archive2.a ...]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " -n dry run (report fixes without applying)\n") + os.Exit(1) + } + + dryRun := false + if args[0] == "-n" { + dryRun = true + args = args[1:] + } + + grandTotal := 0 + for _, path := range args { + if _, err := os.Stat(path); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Warning: %s not found, skipping\n", path) + continue + } + + base := filepath.Base(path) + var fixes int + var err error + + switch { + case strings.HasSuffix(path, ".a"): + fixes, err = processArchive(path, dryRun) + case strings.HasSuffix(path, ".o"): + fixes, err = processObject(path, dryRun) + default: + fmt.Fprintf(os.Stderr, "Skipping %s (not .a or .o)\n", path) + continue + } + + if err != nil { + fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", path, err) + os.Exit(1) + } + + if fixes > 0 { + action := "fixed" + if dryRun { + action = "would fix" + } + fmt.Printf("%s: %s %d literal pool relocation(s)\n", base, action, fixes) + } + grandTotal += fixes + } + + action := "Total fixes applied" + if dryRun { + action = "Total fixes needed" + } + fmt.Printf("%s: %d\n", action, grandTotal) +}