Skip to content

esp32s3: add support for the cores scheduler - #5910

Merged
deadprogram merged 2 commits into
devfrom
esp32s3-cores
Oct 18, 2026
Merged

deadprogram merged 2 commits into
devfrom
esp32s3-cores

Conversation

@deadprogram

@deadprogram deadprogram commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

This PR adds -scheduler=cores to the ESP32-S3, so goroutines can run on both CPUs. The default scheduler is still tasks.

Fixes #5353. This replaces draft PR #5358, thank you for the start on this work.

Commits

  1. targets: fix two context bugs in the esp32s3 interrupt handler. These bugs are already in dev and can also occur in single core programs. The GC stop the world hits both of them.
    • The handler moves a1 to its frame. That register is also the a5/a9/a13 of the caller of the interrupted function. A deep handler spilled that caller's a0..a3 to the wrong address, so the interrupted code returned through garbage. The handler now moves the four words to the place where the underflow reads them.
    • SCOMPARE1 was not saved. If a handler used an atomic between an interrupted wsr.scompare1 and s32c1i, that compare and swap reported success without a store.
  2. esp32s3: add support for the cores scheduler.
    • Core 1 starts at call_start_cpu1 with its own system stack, as in ESP-IDF start_other_core.
    • The FROM_CPU software interrupts on CPU line 3 wake a waiting core and pause it for the GC. All free level 1 lines from 6 to 30 were already in use, so interrupt.New now accepts lines 1 to 30.
    • Spin locks use s32c1i. ticks() takes a lock, because both cores share the timer update latch.
    • interrupt.In, interruptPending and the sleep wake flags are per core.
    • Adds machine.CurrentCore and machine.NumCores. examples/core-assign now also builds for the ESP32-S3.

Cost

  • The second system stack adds 4 KB of RAM to every esp32s3 build, as on rp2040 and rp2350.
  • The unicore code size grows by 72 bytes, from the interrupt handler fixes.

Tested

Tested on a XIAO ESP32-S3:

  • examples/core-assign gives the expected output.
  • A 2 minute stress test passed with no errors: 679 GC cycles and 1.19M interrupts. It ran:
    • a channel ping-pong between the cores
    • allocators on both cores
    • sleepers with different periods on both cores
    • time.Now monotonicity checks on both cores
    • a 10 kHz timer interrupt
  • examples/serial works with both schedulers.
  • With the espradio changes in radio: run the blob on core 0 with the cores scheduler espradio#101, scan, connect with DHCP, http-get, the BLE scanner and ESP-NOW work with both schedulers.

Known limits

  • Only core 0 gets the timer alarm interrupt, so core 1 busy waits in sleepTicks.

The level 1 handler moves a1 to its frame. That register is also the a5,
a9 or a13 of the caller of the interrupted function. So a handler deep
enough to spill that caller saved its a0..a3 below the frame, and not
below the interrupted SP where the underflow reads them back. The
interrupted code then returned through garbage. Copy the four words to
the correct place after the handler returns.

The handler also did not save SCOMPARE1. If a handler used an atomic
between an interrupted wsr.scompare1 and s32c1i, that compare and swap
reported success without a store.

Both can occur in single core programs. The GC stop the world for the
cores scheduler hits both of them.
@github-actions

Copy link
Copy Markdown

Size difference with the dev branch:

Binary size difference
 flash                          ram
 before   after   diff          before   after   diff
  23684   23680     -4  -0.02%    8588    8588      0   0.00% tinygo build -size short -o ./build/test.bin -target=esp32-coreboard-v2 ./examples/gdew0154m09/main.go
  12464   12460     -4  -0.03%    4696    4692     -4  -0.09% tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/basic/
  20044   20040     -4  -0.02%    4964    4964      0   0.00% tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/touchpaint/
  18772   18772      0   0.00%    6876    6876      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/adafruit4650
  63916   63916      0   0.00%    6884    6884      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adt7410/main.go
  11436   11436      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adxl345/main.go
  15612   15612      0   0.00%    7492    7492      0   0.00% tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/amg88xx
  10668   10668      0   0.00%    5396    5396      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/apa102/main.go
  13948   13948      0   0.00%    7252    7252      0   0.00% tinygo build -size short -o ./build/test.hex -target=nano-33-ble ./examples/apds9960/proximity/main.go
  12872   12872      0   0.00%    5408    5408      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/apa102/itsybitsy-m0/main.go
   8856    8856      0   0.00%    2320    2320      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/at24cx/main.go
  10156   10156      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bh1750/main.go
   9452    9452      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/blinkm/main.go
  72320   72320      0   0.00%    3720    3720      0   0.00% tinygo build -size short -o ./build/test.hex -target=pinetime     ./examples/bma42x/main.go
  68540   68540      0   0.00%    6852    6852      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmi160/main.go
  29292   29292      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp180/main.go
  67180   67180      0   0.00%    6884    6884      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go
  13852   13852      0   0.00%    5468    5468      0   0.00% tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bmp388/main.go
  23092   23092      0   0.00%    6116    6116      0   0.00% tinygo build -size short -o ./build/test.hex -target=metro-rp2350 ./examples/bno08x/i2c/main.go
   9256    9256      0   0.00%    3360    3360      0   0.00% tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/sram/main.go
  22900   22900      0   0.00%    3592    3592      0   0.00% tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/time/main.go
  31416   31416      0   0.00%    5668    5668      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/alarms/main.go
  45288   45288      0   0.00%    5668    5668      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/basic/main.go
   4776    4776      0   0.00%    2280    2280      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/easystepper/main.go
   6992    6992      0   0.00%    2344    2344      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/easystepper/async/main.go
  73728   73728      0   0.00%    7712    7712      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/flash/console/spi
  69488   69488      0   0.00%    9752    9752      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/flash/console/qspi
   7556    7556      0   0.00%    2284    2284      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/gc9a01/main.go
  69984   69984      0   0.00%    7084    7084      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/i2c/main.go
  70472   70472      0   0.00%    7188    7188      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/uart/main.go
  10228   10228      0   0.00%    5460    5460      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/hcsr04/main.go
   5896    5896      0   0.00%    2280    2280      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/hd44780/customchar/main.go
   5976    5976      0   0.00%    2280    2280      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/hd44780/text/main.go
  12188   12188      0   0.00%    5420    5420      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/hd44780i2c/main.go
  16828   16828      0   0.00%    7252    7252      0   0.00% tinygo build -size short -o ./build/test.hex -target=nano-33-ble ./examples/hts221/main.go
  16580   16580      0   0.00%    2428    2428      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/hub75/main.go
  11772   11772      0   0.00%    7636    7636      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341/basic
  14252   14252      0   0.00%    5596    5596      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao ./examples/ili9341/basic
  31140   31140      0   0.00%   38796   38796      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341/pyportal_boing
  11748   11748      0   0.00%    7636    7636      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341/scroll
  14276   14276      0   0.00%    5596    5596      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao ./examples/ili9341/scroll
 266524  266524      0   0.00%   47480   47480      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341/slideshow
  12492   12492      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis3dh/main.go
  16236   16236      0   0.00%    7252    7252      0   0.00% tinygo build -size short -o ./build/test.hex -target=nano-33-ble ./examples/lps22hb/main.go
  26616   26616      0   0.00%    2328    2328      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/lsm303agr/main.go
  29900   29900      0   0.00%    7484    7484      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/lsm303dlhc/main.go
  14140   14140      0   0.00%    5444    5444      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/lsm6ds3/main.go
  12716   12716      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mag3110/main.go
  11556   11556      0   0.00%    5428    5428      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp23017/main.go
  11988   11988      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp23017-multiple/main.go
  11868   11868      0   0.00%    5396    5396      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp3008/main.go
  74980   74980      0   0.00%    6964    6964      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp2515/main.go
  30456   30456      0   0.00%    4152    4152      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/microbitmatrix/main.go
  30472   30472      0   0.00%    6196    6196      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit-v2 ./examples/microbitmatrix/main.go
  10188   10188      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mma8653/main.go
  10140   10140      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mpu6050/main.go
  80092   80092      0   0.00%    8124    8124      0   0.00% tinygo build -size short -o ./build/test.hex -target=p1am-100 ./examples/p1am/main.go
  14232   14232      0   0.00%    6056    6056      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/pca9685/main.go
   6692    6692      0   0.00%    3292    3292      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/pcd8544/setbuffer/main.go
   5092    5092      0   0.00%    2284    2284      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/pcd8544/setpixel/main.go
  14280   14280      0   0.00%    6040    6040      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/seesaw/soil-sensor
  13864   13864      0   0.00%    6040    6040      0   0.00% tinygo build -size short -o ./build/test.hex -target=qtpy-rp2040 ./examples/seesaw/rotary-encoder
   2899    2899      0   0.00%     560     560      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino ./examples/servo
  15856   15856      0   0.00%    6120    6120      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico     ./examples/sgp30
   9564    9564      0   0.00%    7436    7436      0   0.00% tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/shifter/main.go
  59072   59072      0   0.00%    3728    3728      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht3x/main.go
  59008   59008      0   0.00%    3728    3728      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht4x/main.go
  59072   59072      0   0.00%    3728    3728      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/shtc3/main.go
  15060   15060      0   0.00%    9132    9132      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao-ble ./examples/ssd1306/
  13736   13736      0   0.00%    6080    6080      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao-rp2040 ./examples/ssd1306/
  13688   13688      0   0.00%    6072    6072      0   0.00% tinygo build -size short -o ./build/test.hex -target=thumby ./examples/ssd1306/
   6388    6388      0   0.00%    2284    2284      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1331/main.go
   7236    7236      0   0.00%    2284    2284      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7735/main.go
   6932    6932      0   0.00%    2284    2284      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7789/main.go
  18844   18844      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/thermistor/main.go
  12764   12764      0   0.00%    5220    5220      0   0.00% tinygo build -size short -o ./build/test.hex -target=circuitplay-bluefruit ./examples/tone
  12108   12108      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/tm1637/main.go
  12716   12716      0   0.00%    6052    6052      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/touch/capacitive
  10932   10932      0   0.00%    7428    7428      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/fourwire/main.go
  13952   13952      0   0.00%    7696    7696      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/pyportal_touchpaint/main.go
  17284   17284      0   0.00%    5508    5508      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl53l1x/main.go
  15956   15956      0   0.00%    5508    5508      0   0.00% tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6180x/main.go
  27208   27208      0   0.00%   14408   14408      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-nrf52840-sense ./examples/waveshare-epd/epd1in54/main.go
   6804    6804      0   0.00%    2324    2324      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13/main.go
   6484    6484      0   0.00%    2316    2316      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go
   6676    6676      0   0.00%    2324    2324      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go
  29896   29896      0   0.00%   19112   19112      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/waveshare-epd/epd2in66b/main.go
   8780    8780      0   0.00%    5428    5428      0   0.00% tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812
   6122    6122      0   0.00%    8464    8464      0   0.00% tinygo build -size short -o ./build/test.bin -target=m5stamp-c3          ./examples/ws2812
  65892   65892      0   0.00%    6636    6636      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go
   1799    1799      0   0.00%     600     600      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino   ./examples/ws2812
   1280    1280      0   0.00%     182     182      0   0.00% tinygo build -size short -o ./build/test.hex -target=digispark ./examples/ws2812
   7776    7776      0   0.00%    8744    8744      0   0.00% tinygo build -size short -o ./build/test.bin -target=xiao-esp32c3 ./examples/ws2812
  33516   33516      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bme280/main.go
  18372   18372      0   0.00%    5380    5380      0   0.00% tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/microphone/main.go
  13116   13116      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/buzzer/main.go
  14092   14092      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/veml6070/main.go
   8540    8540      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/l293x/simple/main.go
  10476   10476      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/l293x/speed/main.go
   8508    8508      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/l9110x/simple/main.go
  10908   10908      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/l9110x/speed/main.go
   8180    8180      0   0.00%    3324    3324      0   0.00% tinygo build -size short -o ./build/test.hex -target=nucleo-f103rb ./examples/shiftregister/main.go
   7596    7596      0   0.00%    2288    2288      0   0.00% tinygo build -size short -o ./build/test.hex -target=hifive1b ./examples/ssd1351/main.go
  15308   15308      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis2mdl/main.go
  13004   13004      0   0.00%    5412    5412      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/max72xx/main.go
  80412   80412      0   0.00%    7032    7032      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/dht/main.go
  38948   38948      0   0.00%    6620    6620      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8523/
  73564   73564      0   0.00%    7072    7072      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/alarm/
   9132    9132      0   0.00%    5428    5428      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/clkout/
  68280   68280      0   0.00%    7068    7068      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/time/
  73420   73420      0   0.00%    7072    7072      0   0.00% tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/timer/
  14440   14440      0   0.00%    6048    6048      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go
  13000   13000      0   0.00%    6040    6040      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/
  10668   10668      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go
  14828   14828      0   0.00%    5436    5436      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina219/main.go
  10600   10600      0   0.00%    5280    5280      0   0.00% tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go
  76948   76948      0   0.00%   11508   11508      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/
  63404   63404      0   0.00%    8892    8892      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/i2csoft/adt7410/
  11772   11772      0   0.00%    7444    7444      0   0.00% tinygo build -size short -o ./build/test.elf -target=wioterminal ./examples/axp192/m5stack-core2-blinky/
  10804   10804      0   0.00%    5980    5980      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/xpt2046/main.go
  14416   14416      0   0.00%    5060    5060      0   0.00% tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/sx126x/lora_rxtx/
  47080   47080      0   0.00%    9608    9608      0   0.00% tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/sx127x/lora_rxtx/
  36128   36128      0   0.00%    7760    7760      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go
  13496   13496      0   0.00%    7032    7032      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go
  14304   14304      0   0.00%    6056    6056      0   0.00% tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/uc8151/main.go
  14080   14080      0   0.00%    6500    6500      0   0.00% tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/waveshare-epd/epd2in9v2/main.go
  15964   15964      0   0.00%    6092    6092      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd30/main.go
  12552   12552      0   0.00%    6060    6060      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd4x/main.go
   9684    9684      0   0.00%    5396    5396      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=circuitplay-express ./examples/makeybutton/main.go
  11388   11388      0   0.00%    5412    5412      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ds18b20/main.go
 125716  125716      0   0.00%    8448    8448      0   0.00% tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/lora/lorawan/atcmd/
  18372   18372      0   0.00%    7644    7644      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/as560x/main.go
  13704   13704      0   0.00%    6040    6040      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu6886/main.go
   9548    9548      0   0.00%    5388    5388      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ttp229/main.go
  70328   70328      0   0.00%    7528    7528      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/ndir/main_ndir.go
  63736   63736      0   0.00%    3792    3792      0   0.00% tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ndir/main_ndir.go
  68252   68252      0   0.00%    6924    6924      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ndir/main_ndir.go
  11400   11400      0   0.00%    6032    6032      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/main.go
  13508   13508      0   0.00%    6100    6100      0   0.00% tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi
  10352   10352      0   0.00%    6472    6472      0   0.00% tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt
  68680   68680      0   0.00%    7488    7488      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/main.go
  45984   45984      0   0.00%    8136    8136      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/tmc2209/main.go
  14084   14084      0   0.00%    5988    5988      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/main.go
  14720   14720      0   0.00%    5216    5216      0   0.00% tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpmem/main.go
  64904   64904      0   0.00%    6664    6664      0   0.00% tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/max6675/main.go
  14872   14872      0   0.00%    6048    6048      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/ens160/main.go
  18416   18416      0   0.00%    6152    6152      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/si5351/main.go
  23856   23856      0   0.00%    7400    7400      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/w5500/main.go
  91656   91656      0   0.00%    7844    7844      0   0.00% tinygo build -size short -o ./build/test.hex -target=pico ./examples/sd
  90508   90508      0   0.00%    7932    7932      0   0.00% tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/
 379280  379280      0   0.00%   14888   14888      0   0.00% tinygo build -size short -o ./build/test.hex -target=pyportal -stack-size 8kb ./examples/net/http-get/
 126948  126948      0   0.00%    9080    9080      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-nano33 -stack-size 8kb ./examples/net/tcpclient/
 357764  357764      0   0.00%   13368   13368      0   0.00% tinygo build -size short -o ./build/test.hex -target=nano-rp2040 -stack-size 8kb ./examples/net/websocket/dial/
 108952  108952      0   0.00%   11060   11060      0   0.00% tinygo build -size short -o ./build/test.hex -target=metro-m4-airlift -stack-size 8kb ./examples/net/socket/
 426744  426744      0   0.00%   17564   17564      0   0.00% tinygo build -size short -o ./build/test.hex -target=matrixportal-m4 -stack-size 8kb ./examples/net/webstatic/
 172864  172864      0   0.00%    9248    9248      0   0.00% tinygo build -size short -o ./build/test.hex -target=arduino-mkrwifi1010 -stack-size 8kb ./examples/net/tlsclient/
 162996  162996      0   0.00%    9836    9836      0   0.00% tinygo build -size short -o ./build/test.hex -target=nano-rp2040 -stack-size 8kb ./examples/net/mqttclient/natiu/
 122932  122932      0   0.00%   14372   14372      0   0.00% tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/webclient/
 375204  375204      0   0.00%   20020   20020      0   0.00% tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/webserver/
 373812  373812      0   0.00%   19080   19080      0   0.00% tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/mqttclient/paho/
 179832  179832      0   0.00%   12312   12312      0   0.00% tinygo build -size short -o ./build/test.hex -target=elecrow-rp2040 -stack-size 8kb ./examples/net/tlsclient/
 126196  126196      0   0.00%   12140   12140      0   0.00% tinygo build -size short -o ./build/test.hex -target=elecrow-rp2350 -stack-size 8kb ./examples/net/ntpclient/
7012252 7012240    -12  -0.00% 1110462 1110458     -4  -0.00%

@davecheney davecheney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some nit picking suggestions about code duplication if you want to address them.

Comment thread src/internal/task/task_stack_esp32.go Outdated

// CurrentCore returns the core number the call was made from. PRID bit 13 is
// the core ID, see xt_utils_get_raw_core_id in esp-idf xt_utils.h.
func CurrentCore() int {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use go:linkname to deduplicate this? Its less about code size and more about DRYing it up so the knowledge of how to get the current core is defined once.

Comment thread src/runtime/interrupt/interrupt_esp32s3_cores.go Outdated
Comment thread src/runtime/interrupt/interrupt_esp32s3_unicore.go Outdated
"unsafe"
)

const numCPU = 2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not in love with the duplication between numCPU and maxCores. I know they're different packages, but ideally the number of cores should be defined once.

// Return the current core number: 0 or 1. PRID bit 13 is the core ID, see
// xt_utils_get_raw_core_id in esp-idf components/xtensa/include/xt_utils.h.
func currentCPU() uint32 {
return uint32(device.AsmFull("rsr {}, PRID", nil)>>13) & 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the third version of currentCore, but it has a different name this time. Definitely time for Atwood's rule of 3s

Comment thread src/runtime/runtime_esp32s3_cores.go
@deadprogram

Copy link
Copy Markdown
Member Author

Thanks @davecheney all of them are addressed.

  1. pause() now uses SystemStack().
  2. runtime.currentCPU is now the only place that reads PRID. machine and runtime/interrupt use go:linkname to call it.
  3. interrupt.In and handleInterrupt now index with currentCPU()&1. The bounds check is gone in the IR for both schedulers.
  4. Done now instead of later. interrupt_esp32s3_cores.go and interrupt_esp32s3_unicore.go are removed. inInterrupt is [2]bool in both builds, one flag for each hardware core. The unicore runtime.currentCPU still returns 0 without reading PRID.
  5. maxCores is gone, so numCPU is the only core count.
  6. Same as 2.
  7. signalCore(core, value) now sets or clears the FROM_CPU interrupt, and the handler and startup use it too.

Retested on a XIAO ESP32-S3. A 130 s stress run with -scheduler=cores passed with 9240 GC cycles and 1.1M timer interrupts, checking interrupt.In and machine.CurrentCore on both cores. The same test with -scheduler=tasks also passed.

@davecheney davecheney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. No notes

@rdon-key

Copy link
Copy Markdown
Contributor

It's exciting to see multicore support coming to the ESP32-S3. Thanks for all the work on this!

I noticed a potential issue with GPIO and I2S DMA interrupt initialization under the cores scheduler.

Both peripherals route their interrupt source to core 0 through INTERRUPT_CORE0:

  • GPIO (CPU interrupt line 10): setupPinInterrupt() in machine_esp32s3.go
  • I2S DMA (CPU interrupt line 18): i2sEnableInterrupt() in machine_esp32s3_i2s.go

However, interrupt.Enable() only sets the bit in the calling core's INTENABLE. If this runs in a goroutine that is currently on core 1, the line is enabled on core 1 instead, so the handler never runs:

  • GPIO: setup runs once via sync.Once, so later SetInterrupt() calls don't fix it.
  • I2S: WriteMono()/WriteStereo() stall once the DMA buffers fill, and ReadMono()/ReadStereo() wait forever for data.

This isn't limited to goroutines assigned to core 1. Any goroutine without an affinity, including main after it blocks, can resume on either core, so the failure could be intermittent.

I haven't reproduced this on hardware, but machine.AssignCore(1) followed by pin.SetInterrupt(...) should show it if I'm reading the code correctly.

If this is a real issue, one option could be to forward Enable() from core 1 to core 0 through the existing signalCore()/crossCoreHandler() path, so peripheral interrupts are always enabled on core 0.

Start core 1 and run goroutines on both cores with -scheduler=cores.
The default scheduler is still tasks.

- Core 1 starts at call_start_cpu1 with its own system stack.
- The FROM_CPU software interrupts on CPU line 3 wake a waiting core and
  pause it for the GC. Lines 1 to 5 are now valid for interrupt.New.
- Spin locks use s32c1i. ticks() takes a lock, because the timer update
  latch is shared by both cores.
- interrupt.In and the sleep wake flags are per core.
- interrupt.Enable on core 1 enables the line on core 0, since
  peripheral interrupts are routed to core 0.
- Add machine.CurrentCore and machine.NumCores.

Fixes #5353. Replaces #5358.
@deadprogram

Copy link
Copy Markdown
Member Author

Thanks @rdon-key you are right, and it reproduces on hardware.

On a XIAO ESP32-S3, a goroutine with machine.AssignCore(1) that calls SetInterrupt and then toggles the pin got 0 of 100 GPIO callbacks. I2S WriteStereo from core 1 stalled after 512 frames.

The fix follows your suggestion. On core 1, interrupt.Enable sets a request bit and signals core 0 through signalCore. crossCoreHandler on core 0 enables the line in its own INTENABLE, then clears the bit. Core 1 waits for that. Core 1 enables its own cross core line directly.

With the fix the same test gets 100 of 100 GPIO callbacks, and I2S runs at about 44.1k frames per second. The stress test still passes with both schedulers.

@deadprogram

Copy link
Copy Markdown
Member Author

Thank you @deadprogram and @rdon-key for review and feedback. Now merging.

@deadprogram
deadprogram merged commit e8cfe79 into dev Oct 18, 2026
40 checks passed
@deadprogram
deadprogram deleted the esp32s3-cores branch October 18, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ESP32S3 support for cores scheduler

3 participants