From 49356b49663400bab46dc1e1590042898f126301 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 25 Jul 2026 12:07:41 +0200 Subject: [PATCH 1/3] perf(esp32): move WiFi statics from SRAM2 to DRAM1 (~14 KB) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Place the RX ring buffer in a .rxring section and ISR/timer arrays in a .wifibss section, both mapped to SRAM1 (DRAM1) by the ESP32 linker script, and then guard the DMA relocation tables. On ESP32 this frees ~14 KB of SRAM2 for the Go GC heap: - RX ring (6 × 1602 B): ~9.6 KB - DMA tables compiled out: ~1.9 KB - ISR + timer arrays: ~2.8 KB Signed-off-by: deadprogram --- isr.c | 16 ++++++++++++---- netif.c | 37 +++++++++++++++++++------------------ osi.c | 9 ++++++++- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/isr.c b/isr.c index 59a2167..9127c8a 100644 --- a/isr.c +++ b/isr.c @@ -76,8 +76,16 @@ void espradio_user_exception(uint32_t cause, uint32_t epc, uint32_t excvaddr, ui /* ---- ISR fn/arg storage ---- */ -static void (*s_isr_fn[32])(void *); -static void *s_isr_arg[32]; +/* On ESP32, place WiFi-only tables in DRAM1 (.wifibss) to free SRAM2 for + * the Go GC heap. On other targets they stay in normal .bss. */ +#if CONFIG_IDF_TARGET_ESP32 +#define WIFIBSS __attribute__((section(".wifibss"))) +#else +#define WIFIBSS +#endif + +static void (*s_isr_fn[32])(void *) WIFIBSS; +static void *s_isr_arg[32] WIFIBSS; /* Bitmask of ISR slots registered via espradio_set_intr (WiFi sources only). */ static uint32_t s_wifi_isr_slots; @@ -140,8 +148,8 @@ void espradio_task_yield_from_isr(void) { static volatile uint32_t s_isr_ring_head; static volatile uint32_t s_isr_ring_tail; static volatile uint32_t s_isr_ring_drops; -static void *s_isr_ring_queue[ESPRADIO_ISR_RING_SIZE]; -static uint8_t s_isr_ring_items[ESPRADIO_ISR_RING_SIZE][ESPRADIO_ISR_ITEM_SIZE]; +static void *s_isr_ring_queue[ESPRADIO_ISR_RING_SIZE] WIFIBSS; +static uint8_t s_isr_ring_items[ESPRADIO_ISR_RING_SIZE][ESPRADIO_ISR_ITEM_SIZE] WIFIBSS; int32_t espradio_queue_send_from_isr(void *queue, void *item, void *hptw) { if (hptw) { diff --git a/netif.c b/netif.c index d34ab77..54b7972 100644 --- a/netif.c +++ b/netif.c @@ -32,27 +32,18 @@ extern void (*mesh_rxcb)(void); static void blob_cb_noop(void) { } -/* pp_wdev_funcs relocation: the ROM ppTask dispatcher calls through a - * heap-allocated function-pointer table (pp_wdev_funcs, 196 entries = 0x310 - * bytes). DMA corruption can zero heap entries at runtime → pc:nil crash. - * After esp_wifi_start() we copy the table into static .bss and redirect - * the ROM pointer there, where DMA cannot reach. */ +/* DMA-corruption workaround tables: relocate heap-allocated function-pointer + * tables to static .bss where DMA cannot reach. Only needed on ESP32-C3/S3; + * on the original ESP32 the blob manages these tables itself and the entry + * counts differ, so these are compiled out to save ~1.9 KB BSS. */ #define PP_WDEV_FUNCS_ENTRIES 196 -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. - * 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 +#if !CONFIG_IDF_TARGET_ESP32 +static uint32_t s_pp_wdev_save[PP_WDEV_FUNCS_ENTRIES]; +static uint32_t s_net80211_funcs_save[NET80211_FUNCS_MAX_ENTRIES]; static uint32_t s_phyFuns_save[PHY_FUNCS_TABLE_WORDS]; +#endif extern void *g_phyFuns; /* ppCheckTxConnTrafficIdle is called only by PM timer callbacks (pm_dream, @@ -213,9 +204,11 @@ void espradio_restore_rom_ptrs(void) { 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. */ + * Redirect it back to our static copy (C3/S3 only). */ +#if !CONFIG_IDF_TARGET_ESP32 if (g_phyFuns != s_phyFuns_save && s_phyFuns_save[0] != 0) g_phyFuns = s_phyFuns_save; +#endif } #define ESPRADIO_NETIF_RXRING_SIZE 6 @@ -226,7 +219,15 @@ typedef struct { uint16_t len; } espradio_rx_frame_t; +/* On ESP32, place the RX ring in the dedicated DRAM1 region (SRAM1 pool 7/6) + * to free ~9.6 KB of SRAM2 for the Go GC heap. The linker script's .rxring + * section sits before the arena in DRAM1. On other targets the ring stays in + * normal .bss. */ +#if CONFIG_IDF_TARGET_ESP32 +static espradio_rx_frame_t s_rx_ring[ESPRADIO_NETIF_RXRING_SIZE] __attribute__((section(".rxring"))); +#else static espradio_rx_frame_t s_rx_ring[ESPRADIO_NETIF_RXRING_SIZE]; +#endif static volatile uint32_t s_rx_head; static volatile uint32_t s_rx_tail; static volatile uint32_t s_rx_cb_count; diff --git a/osi.c b/osi.c index 5c12bd7..b1c49ca 100644 --- a/osi.c +++ b/osi.c @@ -780,6 +780,13 @@ static int espradio_read_mac(uint8_t* mac, unsigned int type) { return rc; } +/* On ESP32, place WiFi-only tables in DRAM1 (.wifibss) to free SRAM2. */ +#if CONFIG_IDF_TARGET_ESP32 +#define WIFIBSS __attribute__((section(".wifibss"))) +#else +#define WIFIBSS +#endif + #define TIMER_SLOTS 32 static struct { void *ptimer; @@ -790,7 +797,7 @@ static struct { bool pending_setfn; uint64_t interval_us; uint64_t deadline_us; -} timer_slots[TIMER_SLOTS]; +} timer_slots[TIMER_SLOTS] WIFIBSS; static unsigned timer_slots_used; void espradio_timer_fire(void *ptimer); From da6521c111f5d454174fdce44692f02f024dcf53 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 25 Jul 2026 17:39:57 +0200 Subject: [PATCH 2/3] debug: update to latest lneto API Signed-off-by: deadprogram --- pcap_debug.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcap_debug.go b/pcap_debug.go index fc18870..2d94bae 100644 --- a/pcap_debug.go +++ b/pcap_debug.go @@ -19,5 +19,5 @@ var _ = pcap.Configure(machine.Serial, xnet.CapturePrinterConfig{ }) func printPacket(msg string, frame []byte) { - pcap.PrintPacket(msg, frame) + pcap.PrintEthernet(msg, frame) } From a0f00f662bb49562cc63ef3c4729f73158478815 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 25 Jul 2026 17:42:31 +0200 Subject: [PATCH 3/3] feat(stack): add AcceptBroadcast4 config for AP/DHCP server mode Wire xnet.StackConfig.AcceptIPv4Broadcast through espradio.StackConfig so AP-mode stacks accept IPv4 broadcast destinations. Required for DHCP server to receive client Discovers/Requests sent to 255.255.255.255. Enable it in netlink/ap.go and examples/ap. Signed-off-by: deadprogram --- espstack.go | 22 +++++++++++++--------- examples/ap/main-ap.go | 7 ++++--- netlink/ap.go | 11 ++++++----- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/espstack.go b/espstack.go index 06b30e2..8fa4915 100644 --- a/espstack.go +++ b/espstack.go @@ -36,6 +36,9 @@ type StackConfig struct { MaxTCPPorts int MaxUDPPorts int PassivePeers int + // AcceptBroadcast4 enables reception of IPv4 broadcast packets. + // Must be true when running a DHCP server (AP mode). + AcceptBroadcast4 bool } // DHCPConfig configures DHCP address acquisition. @@ -57,15 +60,16 @@ func NewStack(dev *NetDev, cfg StackConfig) (*Stack, error) { stack := &Stack{dev: dev} const MTU = MaxFrameSize - ethernet.MaxOverheadSize + 4 // CRC not included:+4 xcfg := xnet.StackConfig{ - DNSServer: cfg.DNSServer, - NTPServer: cfg.NTPServer, - Hostname: cfg.Hostname, - MaxActiveTCPPorts: uint16(cfg.MaxTCPPorts), - MaxActiveUDPPorts: uint16(cfg.MaxUDPPorts), - RandSeed: time.Now().UnixNano() ^ cfg.RandSeed, - HardwareAddress: mac, - MTU: MTU, - PassivePeers: cfg.PassivePeers, + DNSServer: cfg.DNSServer, + NTPServer: cfg.NTPServer, + Hostname: cfg.Hostname, + MaxActiveTCPPorts: uint16(cfg.MaxTCPPorts), + MaxActiveUDPPorts: uint16(cfg.MaxUDPPorts), + RandSeed: time.Now().UnixNano() ^ cfg.RandSeed, + HardwareAddress: mac, + MTU: MTU, + PassivePeers: cfg.PassivePeers, + AcceptIPv4Broadcast: cfg.AcceptBroadcast4, } if cfg.StaticAddress.IsValid() && cfg.StaticAddress.Is4() { xcfg.StaticAddress4 = cfg.StaticAddress.As4() diff --git a/examples/ap/main-ap.go b/examples/ap/main-ap.go index e58c4b7..3b85922 100644 --- a/examples/ap/main-ap.go +++ b/examples/ap/main-ap.go @@ -50,9 +50,10 @@ func main() { println("ap: creating lneto stack...") stack, err := espradio.NewStack(nd, espradio.StackConfig{ - Hostname: ssid, - StaticAddress: addr, - MaxUDPPorts: 2, + Hostname: ssid, + StaticAddress: addr, + MaxUDPPorts: 2, + AcceptBroadcast4: true, }) if err != nil { failure("ap: stack err: " + err.Error()) diff --git a/netlink/ap.go b/netlink/ap.go index a2acac9..bbfa73e 100644 --- a/netlink/ap.go +++ b/netlink/ap.go @@ -102,11 +102,12 @@ func (n *Esplink) NetConnectAP(params APConnectParams) error { udpPorts = 1 // reserve one slot for the DHCP server } espstack, err := espradio.NewStack(nd, espradio.StackConfig{ - Hostname: params.Hostname, - StaticAddress: params.StaticAddr, - MaxUDPPorts: udpPorts, - MaxTCPPorts: params.MaxTCPPorts, - PassivePeers: params.PassivePeers, + Hostname: params.Hostname, + StaticAddress: params.StaticAddr, + MaxUDPPorts: udpPorts, + MaxTCPPorts: params.MaxTCPPorts, + PassivePeers: params.PassivePeers, + AcceptBroadcast4: true, }) if err != nil { if debug {