Visual representation of task switching (and how structures are a natural fit with them)
ke4pjw
Posts: 1,310
I recently updated my light controller software to include a number of improvements using Chip's task switching and structures. I have so much happening in the software that is time sensitive, that I use all of the cogs. One of the key issues was that I only had a single socket for ethernet assigned to the web server. This caused laggy response as each file sent by the server would have to be sent in a serial manner, rather than multiple connections opening and pulling the needed information concurrently. Additionally I wanted mDNS, which also takes up ethernet socket. Doing the book keeping required without task switching or structures would have been beyond my skills. Being out of cogs, there was no way to offload this to other cogs. The task switching in Spin 2 made this easy!
So below is a visual scheme of how all of that fits into one cog. 4 tasks related to the http server. Each also serving as a mdns responder. The scheme includes a dispatcher that checks the interrupts for the sockets to see of work needs to be done. Where structures come into play is so that you can have each of the needed variables local to a task be easily segregated and accessible to that task.
I hope someone finds this useful.
Blitzen24 Code Flow Diagrams
1. Task Initialization Flow
main()
│
├─ ... W6100 init, SD card, config, DNS, mDNS ...
│
├─ Enable SIMR for HTTP + mDNS sockets
│ WRITEBUFFER[0] := %1011_0110 ← Sockets 1,2,4,5,7
│ W6100.writereg(REG_SIMR, ...)
│
├─ Clear residual interrupts (Sn_IRCLR) on sockets 1,2,4,5,7
│
├─ TASKSPIN(1, httpd(80, TCP4, Socket2), @task_stack_1)
│ └─► Task 1 starts on its own stack ──► httpd() with Socket 2
│
├─ TASKSPIN(2, httpd(80, TCP4, Socket5), @task_stack_2)
│ └─► Task 2 starts on its own stack ──► httpd() with Socket 5
│
├─ TASKSPIN(3, httpd(80, TCP4, Socket4), @task_stack_3)
│ └─► Task 3 starts on its own stack ──► httpd() with Socket 4
│
└─ httpd(80, TCP4, Socket1) ← Task 0 runs in main context
└─► Task 0 enters httpd() with Socket 1
┌──────────────────────────────────────────────────────┐
│ MEMORY LAYOUT (per task) │
│ │
│ ctx[t] = HTTPD_CTX struct: │
│ readbuffer[2024] ← receive buffer │
│ filepath[256] ← parsed URL path │
│ webbuff[8192] ← transmit buffer │
│ fh (long) ← SD file handle │
│ rdsize (long) ← response size │
│ psize (word) ← received packet size │
│ mss (long) ← max segment size │
│ │
│socket_ready[4] ← hub flags (dispatcher → task) │
│ task_stack_N[1024] ← 4KB stack per task │
└──────────────────────────────────────────────────────┘
SOCKET ASSIGNMENT:
┌──────────┬──────────┬────────────────┐
│ Task │ Socket │ SIR Bit │
├──────────┼──────────┼────────────────┤
│ Task 0 │ Sock 1 │ %0000_0010 │
│ Task 1 │ Sock 2 │ %0000_0100 │
│ Task 2 │ Sock 5 │ %0010_0000 │
│ Task 3 │ Sock 4 │ %0001_0000 │
│ mDNS │ Sock 7 │ %1000_0000 │
│ E1.31 │ Sock 3 │ (PASM cog) │
│ DNS │ Sock 0 │ (on demand) │
│ HTTP out│ Sock 6 │ (on demand) │
└──────────┴──────────┴────────────────┘
2. TASKNEXT Cooperative Round-Robin Flow
┌──────────────────────────────────────────────────────────┐
│ P2 Cog N — Cooperative Task Scheduler (single cog) │
│ │
│ Task 0 Task 1 Task 2 Task 3 │
│ (Socket 1) (Socket 2) (Socket 5) (Sock 4)│
│ │ │ │ │ │
│ ▼ │ │ │ │
│ ┌──────┐ │ │ │ │
│ │ RUN │ │ │ │ │
│ │ ... │ │ │ │ │
│ │TASK- │ │ │ │ │
│ │NEXT()├──────────►▼ │ │ │
│ └──────┘ ┌──────┐ │ │ │
│ │ RUN │ │ │ │
│ │ ... │ │ │ │
│ │TASK- │ │ │ │
│ │NEXT()├────────────►▼ │ │
│ └──────┘ ┌──────┐ │ │
│ │ RUN │ │ │
│ │ ... │ │ │
│ │TASK- │ │ │
│ │NEXT()├────────────►▼ │
│ └──────┘ ┌──────┐ │
│ │ RUN │ │
│ │ ... │ │
│ ┌───────────────────────────────────────────┤TASK- │ │
│ ▼ │NEXT()│ │
│ ┌──────┐ └──────┘ │
│ │RESUME│ ◄── resumes exactly where Task 0 left off │
│ │ ... │ │
│ └──────┘ │
└──────────────────────────────────────────────────────────┘
KEY: Each task runs until it calls TASKNEXT(), which:
1. Saves current task's execution state (PC, stack pointer)
2. Restores next task's execution state
3. Next task resumes at instruction after its last TASKNEXT()
TASKNEXT() CALL SITES IN httpd():
• Line 890 — idle dispatcher loop (waiting for connection)
• Line 988 — after sending header chunk (static file)
• Line 993 — after sending final header bytes
• Line 1004 — after sending SD file body chunk
• Line 1040 — after sending dynamic response chunk
3. SIR Dispatcher Flow (httpd main loop)
httpd(port, PM, socket) │ │ t := TASKID() │ sockbit := 1 << (socket >> 5) │ ▼ ┌═══════════════════════════════════════════════════════════┐ ║ OUTER LOOP — one iteration per TCP connection ║ ║ ║ ║ ┌─────────────────┐ ┌──────────────────┐ ║ ║ │ firstopen? │─Yes─► W6100.Open() │ ║ ║ │ │ │ ListenTCP() │ ║ ║ └────────┬────────┘ │ firstopen=false │ ║ ║ No └──────────────────┘ ║ ║ │ ║ ║ ┌────────▼────────┐ ║ ║ │ ReopenTCP() │ ← fast: skip MR/PORTR, just ║ ║ │ (4 bus txns) │ CR_Open → CR_LISTEN ║ ║ └────────┬────────┘ ║ ║ │ ║ ║ Clear stale interrupts (Sn_IRCLR) ║ ║ socket_ready[t] := 0 ║ ║ │ ║ ║ ┌════════▼════════════════════════════════════════════┐ ║ ║ ║ INNER LOOP — SIR DISPATCHER (idle polling) ║ ║ ║ ║ ║ ║ ║ ║ ┌──────────────────────────┐ ║ ║ ║ ║ │ sir := W6100.ReadSIR() │ ← 1 bus transaction ║ ║ ║ ║ │ (reads $2101) │ ║ ║ ║ ║ └───────────┬──────────────┘ ║ ║ ║ ║ │ ║ ║ ║ ║ ┌────────▼────────┐ ║ ║ ║ ║ │ sir & sockbit? │──Yes──► ║ ║ ║ ║ └────────┬────────┘ socket_ready[t] = 1 ║ ║ ║ ║ │ ║ ║ ║ ║ ┌────────▼────────┐ ║ ║ ║ ║ │ t == 0? │──No──► skip dispatch ║ ║ ║ ║ └────────┬────────┘ ║ ║ ║ ║ Yes (Task 0 dispatches for all) ║ ║ ║ ║ │ ║ ║ ║ ║┌─────────────▼─────────────────────────┐ ║ ║ ║ ║│ sir & %0000_0100? → socket_ready[1]=1 │ Sk2 ║ ║ ║ ║│ sir & %0010_0000? → socket_ready[2]=1 │ Sk5 ║ ║ ║ ║│ sir & %0001_0000? → socket_ready[3]=1 │ Sk4 ║ ║ ║ ║│ sir & %1000_0000? → mdnscheck() │ Sk7 ║ ║ ║ ║└─────────────┬─────────────────────────┘ ║ ║ ║ ║ │ ║ ║ ║ ║ ┌────────▼──────────────┐ ║ ║ ║ ║ │ socket_ready[t] == 1? │──No──► TASKNEXT() ──┐ ║ ║ ║ ║ └────────┬──────────────┘ │ ║ ║ ║ ║ Yes │ ║ ║ ║ ║ │ │ ║ ║ ║ ║ ┌────────▼───────────────────┐ │ ║ ║ ║ ║ │ EstablishTCP(socket, 1) │ │ ║ ║ ║ ║ │ != Sn_IR_TIMEOUT? │──No──┐ │ ║ ║ ║ ║ └────────┬───────────────────┘ │ │ ║ ║ ║ ║ Yes │ │ ║ ║ ║ ║ │ ┌─────▼───┐ │ ║ ║ ║ ║ QUIT (connected!) │TASKNEXT()│◄───┘ ║ ║ ║ ║ │ └────┬────┘ ║ ║ ║ ║ │ │ ║ ║ ║ ║ │ loop back ────────╝ ║ ║ ╚══════════════╪══════════════════════════════════════╝ ║ ║ │ ║ ║ ┌──────────────▼───────────────────────────────────────┐ ║ ║ │ CONNECTION HANDLING │ ║ ║ │ │ ║ ║ │ Read MSS, ReceiveData(100ms timeout) │ ║ ║ │ Parse GET/POST, extract filepath │ ║ ║ │ │ │ ║ ║ │ ┌───────▼───────┐ │ ║ ║ │ │ File on SD? │──Yes──► Send header + stream file │ ║ ║ │ └───────┬───────┘ (TASKNEXT between chunks) │ ║ ║ │ No │ ║ ║ │ ┌───────▼───────┐ │ ║ ║ │ │ Dynamic .JS? │──Yes──► Generate JS response │ ║ ║ │ │ │ (netjs, e131js, etc.) │ ║ ║ │ └───────┬───────┘ │ ║ ║ │ No │ ║ ║ │ ┌───────▼───────┐ │ ║ ║ │ │ 404 response │ │ ║ ║ │ └───────┬───────┘ │ ║ ║ │ │ │ ║ ║ │ Send response in MSS chunks (TASKNEXT between each) │ ║ ║ │ │ │ ║ ║ │ W6100.Disconnect(socket) │ ║ ║ └──────────┬───────────────────────────────────────────┘ ║ ║ │ ║ ║ loop back to OUTER LOOP (ReopenTCP) ───────────────╝ ╚═══════════════════════════════════════════════════════════╝

Comments
This is very useful thanks.
Is it in chips doc?
Didn’t see it the other day, but maybe it’s tucked away somewhere…
It's in the Spin 2 document. Was introduced in PNUT v47. Yeah, if you have free time in a Spin 2 Cog, you can run up to 32 tasks in it.
Tending to sockets does seem like a perfect use for this…