From a585116bf46990a1823df2854be1185bfcd379a2 Mon Sep 17 00:00:00 2001 From: Hector Chu Date: Tue, 21 Apr 2026 11:58:15 +0100 Subject: [PATCH 1/4] add esp-idf targets --- builder/build.go | 2 +- compileopts/config.go | 2 +- goenv/goenv.go | 2 + loader/loader.go | 1 + src/device/esp/esp32s3-stack.S | 52 ++++++++ src/device/esp/esp32s3.S | 53 -------- src/runtime/atomics_critical.go | 2 +- src/runtime/baremetal.go | 2 +- src/runtime/interrupt/interrupt_none.go | 2 +- .../interrupt/interrupt_tinygoriscv.go | 2 +- src/runtime/interrupt/interrupt_xtensa.go | 2 +- src/runtime/runtime_espidf.go | 113 ++++++++++++++++++ targets/esp32c3-idf.json | 15 +++ targets/esp32s3-idf.json | 14 +++ targets/esp32s3.json | 1 + .../gen-critical-atomics.go | 2 +- 16 files changed, 206 insertions(+), 61 deletions(-) create mode 100644 src/device/esp/esp32s3-stack.S create mode 100644 src/runtime/runtime_espidf.go create mode 100644 targets/esp32c3-idf.json create mode 100644 targets/esp32s3-idf.json diff --git a/builder/build.go b/builder/build.go index 714a331a39..923af2dc40 100644 --- a/builder/build.go +++ b/builder/build.go @@ -189,7 +189,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe defer unlock() libcDependencies = append(libcDependencies, libcJob) libcDependencies = append(libcDependencies, makeMinGWExtraLibs(tmpdir, config.GOARCH())...) - case "": + case "", "none": // no library specified, so nothing to do default: return BuildResult{}, fmt.Errorf("unknown libc: %s", config.Target.Libc) diff --git a/compileopts/config.go b/compileopts/config.go index 1920d2b9cb..05ed0aff42 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -437,7 +437,7 @@ func (c *Config) LibcCFlags() []string { ) } return cflags - case "": + case "", "none": // No libc specified, nothing to add. return nil default: diff --git a/goenv/goenv.go b/goenv/goenv.go index fe4c8bf63e..cecb5a85dd 100644 --- a/goenv/goenv.go +++ b/goenv/goenv.go @@ -151,6 +151,8 @@ func Get(name string) string { panic("could not find cache dir: " + err.Error()) } return filepath.Join(dir, "tinygo") + case "CGO_CFLAGS": + return os.Getenv("CGO_CFLAGS") case "CGO_ENABLED": // Always enable CGo. It is required by a number of targets, including // macOS and the rp2040. diff --git a/loader/loader.go b/loader/loader.go index 1ca1b6679d..c323fb5193 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -485,6 +485,7 @@ func (p *Package) parseFiles() ([]*ast.File, error) { var initialCFlags []string initialCFlags = append(initialCFlags, p.program.config.CFlags(true)...) initialCFlags = append(initialCFlags, "-I"+p.Dir) + initialCFlags = append(initialCFlags, goenv.Get("CGO_CFLAGS")) generated, headerCode, cflags, ldflags, accessedFiles, errs := cgo.Process(files, p.program.workingDir, p.ImportPath, p.program.fset, initialCFlags, p.program.config.GOOS()) p.CFlags = append(initialCFlags, cflags...) p.CGoHeaders = headerCode diff --git a/src/device/esp/esp32s3-stack.S b/src/device/esp/esp32s3-stack.S new file mode 100644 index 0000000000..a4f54a5f9d --- /dev/null +++ b/src/device/esp/esp32s3-stack.S @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------- +// tinygo_scanCurrentStack — Spill all Xtensa register windows to the +// stack, then call tinygo_scanstack(sp) so the conservative GC can +// discover live heap pointers that are currently in physical registers. +// +// On RISC-V / ARM the equivalent function pushes callee-saved registers +// before the call. On Xtensa windowed ABI the same effect is achieved +// by forcing hardware window-overflow for every occupied pane: each +// overflow saves the four registers in that pane to the stack frame +// pointed to by the pane's a1 (sp). After all panes are flushed, a +// scan from the current sp to stackTop covers every live value. +// ----------------------------------------------------------------------- +.section .text.tinygo_scanstack + +.global tinygo_scanCurrentStack +tinygo_scanCurrentStack: + entry a1, 48 + + // Disable interrupts while flushing register windows. + rsr a4, PS + s32i a4, a1, 0 // save PS for later restore + rsil a4, 3 // XCHAL_EXCM_LEVEL + + // Flush all register windows using recursive call4. + // For NAREG=64 (16 panes), 15 recursive levels cover all panes + // except the current one (which is kept active). + movi a6, 15 + call4 .Lscan_spill + + // Restore interrupts. + l32i a4, a1, 0 + wsr.ps a4 + rsync + + // Pass current sp to tinygo_scanstack. + // call4 maps caller's a5→callee's a1 (stack ptr for callee's entry) + // and caller's a6→callee's a2 (first argument = sp). + mov a5, a1 // callee's a1 = valid stack pointer + mov a6, a1 // callee's a2 = sp argument + call4 tinygo_scanstack + + retw + + .balign 4 +.Lscan_spill: + entry a1, 16 + beqz a2, .Lscan_spill_done + addi a2, a2, -1 + mov a6, a2 + call4 .Lscan_spill +.Lscan_spill_done: + retw diff --git a/src/device/esp/esp32s3.S b/src/device/esp/esp32s3.S index 6566e3f342..5572a3d484 100644 --- a/src/device/esp/esp32s3.S +++ b/src/device/esp/esp32s3.S @@ -342,56 +342,3 @@ call_start_cpu0: // If main returns, loop forever. 1: j 1b - -// ----------------------------------------------------------------------- -// tinygo_scanCurrentStack — Spill all Xtensa register windows to the -// stack, then call tinygo_scanstack(sp) so the conservative GC can -// discover live heap pointers that are currently in physical registers. -// -// On RISC-V / ARM the equivalent function pushes callee-saved registers -// before the call. On Xtensa windowed ABI the same effect is achieved -// by forcing hardware window-overflow for every occupied pane: each -// overflow saves the four registers in that pane to the stack frame -// pointed to by the pane's a1 (sp). After all panes are flushed, a -// scan from the current sp to stackTop covers every live value. -// ----------------------------------------------------------------------- -.section .text.tinygo_scanCurrentStack - -.global tinygo_scanCurrentStack -tinygo_scanCurrentStack: - entry a1, 48 - - // Disable interrupts while flushing register windows. - rsr a4, PS - s32i a4, a1, 0 // save PS for later restore - rsil a4, 3 // XCHAL_EXCM_LEVEL - - // Flush all register windows using recursive call4. - // For NAREG=64 (16 panes), 15 recursive levels cover all panes - // except the current one (which is kept active). - movi a6, 15 - call4 .Lscan_spill - - // Restore interrupts. - l32i a4, a1, 0 - wsr.ps a4 - rsync - - // Pass current sp to tinygo_scanstack. - // call4 maps caller's a5→callee's a1 (stack ptr for callee's entry) - // and caller's a6→callee's a2 (first argument = sp). - mov a5, a1 // callee's a1 = valid stack pointer - mov a6, a1 // callee's a2 = sp argument - call4 tinygo_scanstack - - retw - - .balign 4 -.Lscan_spill: - entry a1, 16 - beqz a2, .Lscan_spill_done - addi a2, a2, -1 - mov a6, a2 - call4 .Lscan_spill -.Lscan_spill_done: - retw diff --git a/src/runtime/atomics_critical.go b/src/runtime/atomics_critical.go index 74ce321f10..33173c7901 100644 --- a/src/runtime/atomics_critical.go +++ b/src/runtime/atomics_critical.go @@ -1,4 +1,4 @@ -//go:build baremetal && !tinygo.wasm +//go:build baremetal && !tinygo.wasm && !espidf // Automatically generated file. DO NOT EDIT. // This file implements standins for non-native atomics using critical sections. diff --git a/src/runtime/baremetal.go b/src/runtime/baremetal.go index 9915f191b2..04b931e664 100644 --- a/src/runtime/baremetal.go +++ b/src/runtime/baremetal.go @@ -1,4 +1,4 @@ -//go:build baremetal +//go:build baremetal && !espidf package runtime diff --git a/src/runtime/interrupt/interrupt_none.go b/src/runtime/interrupt/interrupt_none.go index ea8bdb68c6..b8e009e142 100644 --- a/src/runtime/interrupt/interrupt_none.go +++ b/src/runtime/interrupt/interrupt_none.go @@ -1,4 +1,4 @@ -//go:build !baremetal || tkey +//go:build !baremetal || tkey || espidf package interrupt diff --git a/src/runtime/interrupt/interrupt_tinygoriscv.go b/src/runtime/interrupt/interrupt_tinygoriscv.go index 558e67150c..4e897eaa73 100644 --- a/src/runtime/interrupt/interrupt_tinygoriscv.go +++ b/src/runtime/interrupt/interrupt_tinygoriscv.go @@ -1,4 +1,4 @@ -//go:build tinygo.riscv && !tkey +//go:build tinygo.riscv && !tkey && !espidf package interrupt diff --git a/src/runtime/interrupt/interrupt_xtensa.go b/src/runtime/interrupt/interrupt_xtensa.go index bbe70afb6f..67206d139d 100644 --- a/src/runtime/interrupt/interrupt_xtensa.go +++ b/src/runtime/interrupt/interrupt_xtensa.go @@ -1,4 +1,4 @@ -//go:build xtensa && !esp32s3 +//go:build xtensa && !esp32s3 && !espidf package interrupt diff --git a/src/runtime/runtime_espidf.go b/src/runtime/runtime_espidf.go new file mode 100644 index 0000000000..195d7c05c5 --- /dev/null +++ b/src/runtime/runtime_espidf.go @@ -0,0 +1,113 @@ +//go:build espidf + +package runtime + +var ( + heapStart uintptr + heapEnd uintptr + globalsStart uintptr + globalsEnd uintptr + stackTop uintptr +) + +// Allows C consumers of the library to set the GC variables. +// +//export tinygo_init +func tinygo_init(heap, heapSize, glob, globEnd, stack uintptr) { + heapStart, heapEnd = heap, heap+heapSize + globalsStart, globalsEnd = glob, globEnd + stackTop = stack + initRand() + initHeap() + initAll() +} + +func growHeap() bool { + return false +} + +//export abort +func abort() + +//export exit +func exit(code int) + +//export putchar +func libc_putchar(c byte) + +func putchar(c byte) { + libc_putchar(c) +} + +//export getchar +func libc_getchar() byte + +func getchar() byte { + return libc_getchar() +} + +func buffered() int { + return 0 +} + +type timespec struct { + tv_sec int64 + tv_nsec int32 +} + +//export clock_gettime +func clock_gettime(clk_id int32, ts *timespec) + +func getTime(clock int32) uint64 { + ts := timespec{} + clock_gettime(clock, &ts) + return uint64(ts.tv_sec)*1000*1000*1000 + uint64(ts.tv_nsec) +} + +const clock_MONOTONIC = 1 + +func monotime() uint64 { + return getTime(clock_MONOTONIC) +} + +func ticks() timeUnit { + return timeUnit(monotime()) +} + +func ticksToNanoseconds(ticks timeUnit) int64 { + return int64(ticks) +} + +func nanosecondsToTicks(ns int64) timeUnit { + return timeUnit(ns) +} + +//export nanosleep +func nanosleep(req, rem *timespec) int + +func sleepTicks(d timeUnit) { + nanosleep(×pec{int64(d / 1e9), int32(d % 1e9)}, nil) +} + +const baremetal = true + +//go:linkname now time.now +func now() (sec int64, nsec int32, mono int64) { + mono = nanotime() + sec = mono / 1e9 + nsec = int32(mono % 1e9) + return +} + +// Picolibc is not configured to define its own errno value, instead it calls +// __errno_location. +// TODO: a global works well enough for now (same as errno on Linux with +// -scheduler=tasks), but this should ideally be a thread-local variable stored +// in task.Task. +// Especially when we add multicore support for microcontrollers. +var errno int32 + +//export __errno_location +func libc_errno_location() *int32 { + return &errno +} diff --git a/targets/esp32c3-idf.json b/targets/esp32c3-idf.json new file mode 100644 index 0000000000..540e9842e3 --- /dev/null +++ b/targets/esp32c3-idf.json @@ -0,0 +1,15 @@ +{ + "inherits": ["riscv32"], + "features": "+32bit,+c,+m,+zmmul,-a,-b,-d,-e,-experimental-sdext,-experimental-sdtrig,-experimental-smctr,-experimental-ssctr,-experimental-svukte,-experimental-xqcia,-experimental-xqciac,-experimental-xqcicli,-experimental-xqcicm,-experimental-xqcics,-experimental-xqcicsr,-experimental-xqciint,-experimental-xqcilo,-experimental-xqcilsm,-experimental-xqcisls,-experimental-zalasr,-experimental-zicfilp,-experimental-zicfiss,-experimental-zvbc32e,-experimental-zvkgs,-f,-h,-relax,-sha,-shcounterenw,-shgatpa,-shtvala,-shvsatpa,-shvstvala,-shvstvecd,-smaia,-smcdeleg,-smcsrind,-smdbltrp,-smepmp,-smmpm,-smnpm,-smrnmi,-smstateen,-ssaia,-ssccfg,-ssccptr,-sscofpmf,-sscounterenw,-sscsrind,-ssdbltrp,-ssnpm,-sspm,-ssqosid,-ssstateen,-ssstrict,-sstc,-sstvala,-sstvecd,-ssu64xl,-supm,-svade,-svadu,-svbare,-svinval,-svnapot,-svpbmt,-svvptc,-v,-xcvalu,-xcvbi,-xcvbitmanip,-xcvelw,-xcvmac,-xcvmem,-xcvsimd,-xesppie,-xmipscmove,-xmipslsp,-xsfcease,-xsfvcp,-xsfvfnrclipxfqf,-xsfvfwmaccqqq,-xsfvqmaccdod,-xsfvqmaccqoq,-xsifivecdiscarddlone,-xsifivecflushdlone,-xtheadba,-xtheadbb,-xtheadbs,-xtheadcmo,-xtheadcondmov,-xtheadfmemidx,-xtheadmac,-xtheadmemidx,-xtheadmempair,-xtheadsync,-xtheadvdot,-xventanacondops,-xwchc,-za128rs,-za64rs,-zaamo,-zabha,-zacas,-zalrsc,-zama16b,-zawrs,-zba,-zbb,-zbc,-zbkb,-zbkc,-zbkx,-zbs,-zca,-zcb,-zcd,-zce,-zcf,-zcmop,-zcmp,-zcmt,-zdinx,-zfa,-zfbfmin,-zfh,-zfhmin,-zfinx,-zhinx,-zhinxmin,-zic64b,-zicbom,-zicbop,-zicboz,-ziccamoa,-ziccif,-zicclsm,-ziccrse,-zicntr,-zicond,-zicsr,-zifencei,-zihintntl,-zihintpause,-zihpm,-zimop,-zk,-zkn,-zknd,-zkne,-zknh,-zkr,-zks,-zksed,-zksh,-zkt,-ztso,-zvbb,-zvbc,-zve32f,-zve32x,-zve64d,-zve64f,-zve64x,-zvfbfmin,-zvfbfwma,-zvfh,-zvfhmin,-zvkb,-zvkg,-zvkn,-zvknc,-zvkned,-zvkng,-zvknha,-zvknhb,-zvks,-zvksc,-zvksed,-zvksg,-zvksh,-zvkt,-zvl1024b,-zvl128b,-zvl16384b,-zvl2048b,-zvl256b,-zvl32768b,-zvl32b,-zvl4096b,-zvl512b,-zvl64b,-zvl65536b,-zvl8192b", + "build-tags": ["espidf", "esp"], + "scheduler": "none", + "rtlib": "none", + "libc": "none", + "cflags": [ + "-march=rv32imc" + ], + "ldflags": [ + "-r", + "--no-gc-sections" + ] +} diff --git a/targets/esp32s3-idf.json b/targets/esp32s3-idf.json new file mode 100644 index 0000000000..3bf9dac8c1 --- /dev/null +++ b/targets/esp32s3-idf.json @@ -0,0 +1,14 @@ +{ + "inherits": ["xtensa"], + "cpu": "esp32s3", + "features": "+atomctl,+bool,+clamps,+coprocessor,+debug,+density,+div32,+esp32s3,+exception,+fp,+highpriinterrupts,+interrupt,+loop,+mac16,+memctl,+minmax,+miscsr,+mul32,+mul32high,+nsa,+prid,+regprotect,+rvector,+s32c1i,+sext,+threadptr,+timerint,+windowed", + "build-tags": ["espidf", "esp"], + "linker": "ld.lld", + "ldflags": [ + "-r", + "--no-gc-sections" + ], + "extra-files": [ + "src/device/esp/esp32s3-stack.S" + ] +} diff --git a/targets/esp32s3.json b/targets/esp32s3.json index ea6527d6d5..a1c3c28475 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -20,6 +20,7 @@ "linkerscript": "targets/esp32s3.ld", "extra-files": [ "src/device/esp/esp32s3.S", + "src/device/esp/esp32s3-stack.S", "targets/esp32s3-interrupts.S", "src/internal/task/task_stack_esp32.S" ], diff --git a/tools/gen-critical-atomics/gen-critical-atomics.go b/tools/gen-critical-atomics/gen-critical-atomics.go index 98ceebb020..a8ce6f3819 100644 --- a/tools/gen-critical-atomics/gen-critical-atomics.go +++ b/tools/gen-critical-atomics/gen-critical-atomics.go @@ -17,7 +17,7 @@ var tmpl = template.Must(template.New("go").Funcs(template.FuncMap{ return v }, "title": strings.Title, -}).Parse(`//go:build baremetal && !tinygo.wasm +}).Parse(`//go:build baremetal && !tinygo.wasm && !espidf // Automatically generated file. DO NOT EDIT. // This file implements standins for non-native atomics using critical sections. From e97a6e279af098d980d6139a85c843f0405dfca9 Mon Sep 17 00:00:00 2001 From: Hector Chu Date: Tue, 21 Apr 2026 12:45:59 +0100 Subject: [PATCH 2/4] add rand --- src/crypto/rand/rand_espidf.go | 22 ++++++++++++++++++++++ src/runtime/rand_norng.go | 2 +- src/runtime/runtime_espidf.go | 10 ++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/crypto/rand/rand_espidf.go diff --git a/src/crypto/rand/rand_espidf.go b/src/crypto/rand/rand_espidf.go new file mode 100644 index 0000000000..bf18b4aa16 --- /dev/null +++ b/src/crypto/rand/rand_espidf.go @@ -0,0 +1,22 @@ +//go:build espidf + +package rand + +import "unsafe" + +func init() { + Reader = &reader{} +} + +type reader struct{} + +//export esp_fill_random +func esp_fill_random(buf unsafe.Pointer, len uintptr) + +func (r *reader) Read(b []byte) (n int, err error) { + if len(b) == 0 { + return + } + esp_fill_random(unsafe.Pointer(&b[0]), uintptr(len(b))) + return len(b), nil +} diff --git a/src/runtime/rand_norng.go b/src/runtime/rand_norng.go index b9ab475c76..53de54f328 100644 --- a/src/runtime/rand_norng.go +++ b/src/runtime/rand_norng.go @@ -1,4 +1,4 @@ -//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1 || stm32g0)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || esp32s3 || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350) +//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1 || stm32g0)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || esp32s3 || espidf || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350) package runtime diff --git a/src/runtime/runtime_espidf.go b/src/runtime/runtime_espidf.go index 195d7c05c5..0533346af9 100644 --- a/src/runtime/runtime_espidf.go +++ b/src/runtime/runtime_espidf.go @@ -2,6 +2,8 @@ package runtime +import "unsafe" + var ( heapStart uintptr heapEnd uintptr @@ -111,3 +113,11 @@ var errno int32 func libc_errno_location() *int32 { return &errno } + +//export esp_fill_random +func esp_fill_random(buf unsafe.Pointer, len uintptr) + +func hardwareRand() (n uint64, ok bool) { + esp_fill_random(unsafe.Pointer(&n), 8) + return n, true +} From a22ae765ed1775b31dbc4b02e5bff99a5b431a2e Mon Sep 17 00:00:00 2001 From: Hector Chu Date: Tue, 21 Apr 2026 13:30:31 +0100 Subject: [PATCH 3/4] fix sleep --- src/runtime/runtime_espidf.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runtime/runtime_espidf.go b/src/runtime/runtime_espidf.go index 0533346af9..1b1ddb199c 100644 --- a/src/runtime/runtime_espidf.go +++ b/src/runtime/runtime_espidf.go @@ -63,7 +63,7 @@ func clock_gettime(clk_id int32, ts *timespec) func getTime(clock int32) uint64 { ts := timespec{} clock_gettime(clock, &ts) - return uint64(ts.tv_sec)*1000*1000*1000 + uint64(ts.tv_nsec) + return uint64(ts.tv_sec)*1e9 + uint64(ts.tv_nsec) } const clock_MONOTONIC = 1 @@ -84,11 +84,11 @@ func nanosecondsToTicks(ns int64) timeUnit { return timeUnit(ns) } -//export nanosleep -func nanosleep(req, rem *timespec) int +//export usleep +func usleep(usec uint) int func sleepTicks(d timeUnit) { - nanosleep(×pec{int64(d / 1e9), int32(d % 1e9)}, nil) + usleep(uint(d / 1e3)) } const baremetal = true From d7118240fa4db75551ee2a3a80c566a1d8835d76 Mon Sep 17 00:00:00 2001 From: Hector Chu Date: Tue, 21 Apr 2026 14:31:07 +0100 Subject: [PATCH 4/4] fix time now --- src/runtime/runtime_espidf.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/runtime/runtime_espidf.go b/src/runtime/runtime_espidf.go index 1b1ddb199c..63f188d032 100644 --- a/src/runtime/runtime_espidf.go +++ b/src/runtime/runtime_espidf.go @@ -52,22 +52,25 @@ func buffered() int { return 0 } +const ( + clock_REALTIME = 1 + clock_MONOTONIC = 4 +) + type timespec struct { tv_sec int64 tv_nsec int32 } //export clock_gettime -func clock_gettime(clk_id int32, ts *timespec) +func clock_gettime(clock int32, ts *timespec) func getTime(clock int32) uint64 { - ts := timespec{} + var ts timespec clock_gettime(clock, &ts) return uint64(ts.tv_sec)*1e9 + uint64(ts.tv_nsec) } -const clock_MONOTONIC = 1 - func monotime() uint64 { return getTime(clock_MONOTONIC) } @@ -95,9 +98,11 @@ const baremetal = true //go:linkname now time.now func now() (sec int64, nsec int32, mono int64) { + var ts timespec + clock_gettime(clock_REALTIME, &ts) + sec = ts.tv_sec + nsec = ts.tv_nsec mono = nanotime() - sec = mono / 1e9 - nsec = int32(mono % 1e9) return }