From 878ac09b3c370c541803ef4bc9b00763043ed766 Mon Sep 17 00:00:00 2001 From: Fabio Rossetto Date: Thu, 27 Aug 2026 19:40:06 -0300 Subject: [PATCH] esp32: fix linker layout, UART/I2C clocking, and print interleaving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by cross-checking the esp32-generic target against the real esp-idf sources, after confirming the WiFi/BT blob crashes traced back here rather than to the blob itself. - targets/esp32.ld: IROM region was sized at a flat 4M instead of the real SOC_IROM_HIGH - SOC_IROM_LOW (0x330000) limit, and the .iram section pattern list was missing *(.coexsleepiram*), a WiFi/BT-blob section confirmed present via objdump on libcoexist.a. An unmapped section here means the blob's power-management ISR runs out of whatever memory happened to follow, which shows up as sporadic crashes during radio activity. - src/machine/machine_esp32.go: UART never set CONF0_TICK_REF_ALWAYS_ON, which only worked by luck on UART0 because the ROM bootloader happens to leave it in APB clock mode; any other UART or a cold path through this code gets the wrong clock source. Also fixes the baud divider to keep its fractional part instead of truncating to a bare integer division. - src/machine/machine_esp32_i2c.go: SCL_HIGH_PERIOD compensation was scaled by clock frequency (halfCycle/8) instead of using the fixed offset esp-idf's noise-filter formula actually calls for (thres+6, thres=7 → 13), producing wrong SCL timing at anything other than the frequency it happened to be tested at. - src/runtime/scheduler_cooperative.go: printlock/printunlock were no-ops, so two goroutines calling println concurrently can interleave their output mid-line on this cooperative scheduler (print can yield, e.g. waiting on a UART FIFO, before it finishes writing). Added a real bounded-wait lock. go.mod/go.sum: point tinygo.org/x/espflasher at flrossetto/espflasher (fix-esp32-chip-detect-and-stub-baud-cap) until that fix lands upstream — see that repo for details on the chip-detection and flash baud-rate fixes needed for reliable esp32-generic flashing. --- go.mod | 2 ++ go.sum | 4 ++-- src/machine/machine_esp32.go | 22 +++++++++++++++++++++- src/machine/machine_esp32_i2c.go | 16 +++++++++++----- src/runtime/scheduler_cooperative.go | 18 ++++++++++++++++-- targets/esp32.ld | 10 +++++++++- 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 5360718d6d..4e9ddae396 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,8 @@ require ( tinygo.org/x/go-llvm v0.0.0-20260721072906-185673ef46a5 ) +replace tinygo.org/x/espflasher => github.com/flrossetto/espflasher v0.0.0-20260827223712-de27f3043f8f + require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/chavacava/garif v0.1.0 // indirect diff --git a/go.sum b/go.sum index b54514bf75..0e6f26f858 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/flrossetto/espflasher v0.0.0-20260827223712-de27f3043f8f h1:KzjN0vDU6jEjCmhquglQqqETitPCidw8LUJVrXbUcT8= +github.com/flrossetto/espflasher v0.0.0-20260827223712-de27f3043f8f/go.mod h1:YLkCtOCz6gdrbueTi6uZbGPSYduTYt62JdNGmiGxgc0= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= @@ -118,7 +120,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -tinygo.org/x/espflasher v0.8.1 h1:Vp+xA16af9NKVOOSJCcrJHqPSLDCpbfCd/TG3x449ks= -tinygo.org/x/espflasher v0.8.1/go.mod h1:YLkCtOCz6gdrbueTi6uZbGPSYduTYt62JdNGmiGxgc0= tinygo.org/x/go-llvm v0.0.0-20260721072906-185673ef46a5 h1:0PKRhM1INWAi7PdIng1BHMPTDaRcOmv1UymdTgZ76Jo= tinygo.org/x/go-llvm v0.0.0-20260721072906-185673ef46a5/go.mod h1:GFbusT2VTA4I+l4j80b17KFK+6whv69Wtny5U+T8RR0= diff --git a/src/machine/machine_esp32.go b/src/machine/machine_esp32.go index 8f6ddcc018..67dd62d281 100644 --- a/src/machine/machine_esp32.go +++ b/src/machine/machine_esp32.go @@ -539,7 +539,27 @@ func (uart *UART) Configure(config UARTConfig) error { } } - uart.Bus.CLKDIV.Set(peripheralClock / config.BaudRate) + // Select APB (80MHz) as the UART source clock. ESP-IDF always writes + // this explicitly (uart_ll_set_sclk, UART_SCLK_DEFAULT = APB, + // components/soc/esp32/include/soc/clk_tree_defs.h:238-240) rather than + // relying on the register's power-on-reset state — CONF0.TICK_REF_ALWAYS_ON + // selects APB when 1, REF_TICK (~1MHz) when 0 (components/esp_hal_uart/ + // esp32/include/hal/uart_ll.h:179-189). Without this, UART0 happens to + // work because the ROM bootloader already leaves it in APB mode for its + // own console output, but UART1/UART2 (never touched by ROM) have no such + // guarantee, and the CLKDIV math below assumes an 80MHz source either way. + uart.Bus.SetCONF0_TICK_REF_ALWAYS_ON(1) + + // UART_CLKDIV_REG packs a 20-bit integer divider (bits 19:0) and a 4-bit + // fractional divider (bits 23:20) — see components/soc/esp32/register/ + // soc/uart_struct.h:123-129. Writing a plain integer division result + // (as before) zeroes the fractional part, which is usually a negligible + // rounding error at low baud rates but can be a significant fraction of + // the divider at high baud rates (small divider values). Compute both + // parts the way ESP-IDF's uart_ll_set_baudrate does (components/ + // esp_hal_uart/esp32/include/hal/uart_ll.h:265-278): clk_div = (sclk<<4)/baud. + clkDiv := (uint32(peripheralClock) << 4) / config.BaudRate + uart.Bus.CLKDIV.Set((clkDiv >> 4) | ((clkDiv & 0xf) << 20)) if config.RX != NoPin { config.RX.configure(PinConfig{Mode: PinInputPullup}, uart.txrxSignal) diff --git a/src/machine/machine_esp32_i2c.go b/src/machine/machine_esp32_i2c.go index abcc2b4c94..36c5c761f6 100644 --- a/src/machine/machine_esp32_i2c.go +++ b/src/machine/machine_esp32_i2c.go @@ -124,11 +124,17 @@ func (i2c *I2C) initFrequency() { halfCycle := sclkFreq / i2c.config.Frequency / 2 //SCL sclLow := halfCycle - sclWaitHigh := uint32(0) - if i2c.config.Frequency > 50000 { - sclWaitHigh = halfCycle / 8 // compensate the time when freq > 50K - } - sclHigh := halfCycle - sclWaitHigh + // SCL_HIGH_PERIOD must be reduced to compensate for the SCL noise + // filter's propagation delay, per the TRM (see i2c_ll_master_set_bus_timing + // in components/esp_hal_i2c/esp32/include/hal/i2c_ll.h:110-127): with the + // filter enabled (initNoiseFilter sets SCL_FILTER_CFG=0xF, i.e. thres=7, + // en=1 — bit layout in components/soc/esp32/register/soc/i2c_struct.h:230-236), + // the correct compensation for thres in [3,7] is thres+6 = 13, a fixed + // amount independent of the bus frequency (not halfCycle/8, and not only + // applied above 50kHz — ESP-IDF applies it unconditionally whenever the + // filter is enabled). + const sclHighFilterCompensation = 13 + sclHigh := halfCycle - sclHighFilterCompensation // SDA sdaHold := halfCycle / 4 sda_sample := halfCycle / 2 diff --git a/src/runtime/scheduler_cooperative.go b/src/runtime/scheduler_cooperative.go index 72d9e175cf..706fd34004 100644 --- a/src/runtime/scheduler_cooperative.go +++ b/src/runtime/scheduler_cooperative.go @@ -326,10 +326,24 @@ func unlockAtomics(mask interrupt.State) { interrupt.Restore(mask) } +// printLocked serializes concurrent goroutines' print output. Without it, +// two goroutines calling println at the same time on this cooperative +// scheduler can interleave their output mid-line, since a print call can +// yield (e.g. while waiting on a UART FIFO) before it finishes writing. +var printLocked bool + +const printlockMaxWait = 10000 + func printlock() { - // nothing to do + for i := 0; printLocked; i++ { + if i >= printlockMaxWait { + break + } + Gosched() + } + printLocked = true } func printunlock() { - // nothing to do + printLocked = false } diff --git a/targets/esp32.ld b/targets/esp32.ld index 0c2e359391..0056b7bd2f 100644 --- a/targets/esp32.ld +++ b/targets/esp32.ld @@ -37,7 +37,14 @@ MEMORY IRAM (x) : ORIGIN = 0x40080000, LENGTH = 128K /* SRAM0 (usable portion) */ DROM (r) : ORIGIN = 0x3F400000, LENGTH = 4M /* Flash data bus (page-aligned) */ - IROM (rx) : ORIGIN = 0x400D0000, LENGTH = 4M /* Flash instruction bus (page-aligned) */ + + /* IROM's real hardware ceiling is SOC_IROM_HIGH = 0x40400000 + * (components/soc/esp32/include/soc/soc.h:166-167 in esp-idf), only + * 0x330000 (3.1875M) above SOC_IROM_LOW = 0x400D0000 — NOT 4M. A prior + * 4M declaration here silently accepted a .text large enough to map + * past the real flash-cache-mappable range into invalid address space; + * ld would not catch it until (or unless) an app grew that large. */ + IROM (rx) : ORIGIN = 0x400D0000, LENGTH = 0x330000 /* Flash instruction bus (page-aligned) */ } ENTRY(call_start_cpu0) @@ -174,6 +181,7 @@ SECTIONS *(.wifiorslpiram*) *(.iram1*) *(.coexiram*) + *(.coexsleepiram*) . = ALIGN(4); _iram_end = .;