CYD Studio › Docs › Run modes and memory
Run modes and memory
Every project row has an N / T / S chip. Click to cycle; the choice is saved per file (config.json modlar). Default N. The selected file's bar: Save · N|T|S · Run/Stop (one button) · Delete; it turns into Stop while running and "Start solo" when S is selected.
The three modes
| Mode | How it runs | Heap | Internal DRAM | For |
|---|---|---|---|---|
| N Normal | asyncio task on one shared thread | 2–15 KB | 0 (shared worker 17 KB once) | LEDs, sensors, small pages: most projects |
| T Thread | Its own FreeRTOS thread, sliced by the GIL (ESP32 only) | same | 10–15 KB | Display drawing, tight loops without sleep, heavy compute |
| S Solo | Panel off, board reboots, single project | all | all | BLE, HTTPS |
How normal mode works: the project file is wrapped line by line into an async def, top-level names are declared global, cyd.sleep( → await, time.sleep( → await. The result is streamed to a temp file and compiled from the file with execfile; a 12 KB source never sits in the heap as a string. If a sleep call is inside a def body the transform does not apply (MicroPython silently turns a plain def containing await into a generator and the body never runs); a pre-scan catches that and the project opens as T by itself, the log says why. If you write async def yourself it is left alone and the main() coroutine is run.
Solo is one-shot: main.py deletes the flag first thing; pressing RESET brings the panel back. Panel: select the file → S → Run → WiFi on/off question. API: POST /api/solo?name=<file>&wifi=0|1.
Two memories
The ESP32 has two separate pockets, and the second one is the critical one:
| MicroPython heap | Project data, gc.mem_free() |
|---|---|
| Internal DRAM | WiFi, sockets, thread stacks, BLE, TLS. cyd.dram() |
gc.mem_free() can look generous while a thread cannot be created or BLE/TLS returns ENOMEM. Measure with the panel's system box or cyd.dram().
| CYD | Stock v1.28.0 | CYD Studio firmware |
|---|---|---|
| Internal DRAM free, panel up | 27.8 KB | 55 KB |
| Python heap free | 80 / 137 (auto-grows, swallows DRAM) | 75 / 94 (fixed 96 KB) |
| Concurrent projects | 1 (2nd: can't create thread) | 7 normal + 2–3 threads |
What the firmware changed: 12 core modules frozen into flash; the heap is fixed (auto-doubling dropped DRAM from 76 to 12 KB with two small projects); smaller lwIP/WiFi buffers; live log ring 250 → 100 lines (250 lines held 20.7 KB and fragmented the heap); server socket timeout 5 seconds.
Thread stacks come out of internal DRAM: 10–15 KB per project. A project drawing on the display takes 25–33 KB of heap (returned on stop). No leak: same project started/stopped 4× → 65.7 / 65.7 / 65.9 / 65.8 KB.
Measured: projects side by side (CYD)
| Started in order | From heap | From DRAM | Running |
|---|---|---|---|
yuk_kontrol (12 KB source + /yuk routes) | 12.2 KB | 17.3 KB (shared worker, once) | 1 |
| led_yakip_sondur | 2.2 | 0 | 2 |
| gece_lambasi | 8.2 | 0.3 | 3 |
| ornek_led | 4.7 | 0 | 4 |
| isik_olcum | 7.7 | 0 | 5 |
| harici_led | 7.1 | 0 | 7 |
| ornek_isik_sensoru (draws on the display) | 33 KB → heap 1 KB | 0 | 8 |
Seven small projects side by side with internal RAM flat at 37 KB; the limit is now the heap. If you need a bigger heap: frozen_esp32_build.py --heap 112 (DRAM 55 → 39 KB, still room for 2 threads).
Truly side by side?
A "button poller" project looping every 10 ms; longest gap between turns over 15 seconds:
| Scenario | average | max | gaps >100 ms |
|---|---|---|---|
| Alone, normal | 12.9 ms | 21 ms | 0 |
| + 4 normal projects (LED/LDR) | 15.9 | 352 | 5 |
| + a display-drawing project (normal) | 25.5 | 973 | 33 |
| + a thread doing 90% CPU math | 211 | 3405 | 5 |
| Same load, poller in T | 12.0 | 37 | 0 |
Reading: next to LED/sensor neighbours normal mode really alternates. A display-drawing project is a bad neighbour in normal mode (an SPI fill holds the single thread for ~1 s): make it T. A thread that never yields the CPU starves the normal worker: put cyd.sleep(0.001) in the heavy loop or make the input project T. For buttons the real answer is cyd.buton (interrupt), not polling.
Stopping and the watchdog
Threads do not isolate: MicroPython has a GIL, one thread runs bytecode at a time. A project that never yields the CPU starves the panel. The protection:
- The server writes a heartbeat every turn; the listening socket has a timeout so
accept()cannot stall it. - When the first job starts a watchdog thread checks the heartbeat every 2 seconds.
- If the panel misses heartbeats for 20 seconds, running projects get the stop flag.
- If a project does not stop within 4 seconds,
machine.reset(). The panel always comes back; verified: a choking project was stopped at second 28 and the panel returned to 302 ms.
On the ESP8266
There is no _thread. N = a task in the server's asyncio loop (the T chip is hidden), S is available. Measured: 3 normal projects side by side, heap 19 → 9 KB (2.5–5 KB each), panel max 0.69 s. Practical limit 3 projects. For a project that never calls cyd.sleep() a timer pre-emptor checks every 100 ms and, if the pump has not run for 400 ms, slips a server turn between two bytecodes; with while True: x += 1 running, 6/6 status queries were answered. If a stop request is ignored for 4.5 seconds the log gets !!! yanit vermiyor and the board resets (the panel is back in ~10 s).
Last updated 2026-10-17 · This page as Markdown