044 Project 44: TinyGo Cooperative Tasks (Soft RT)

Updated

July 30, 2026

044 TinyGo: Cooperative Multi-Tasking Without a Full RTOS

Implement a tiny cooperative scheduler: multiple tasks share one core by yielding, with per-task periods—useful mental model for soft realtime systems and for understanding why preemptive RTOS exists.

Theory (short)

Model Description
Cooperative Task runs until it yields; bad task starves others
Preemptive RTOS Timer IRQ switches tasks; needs careful shared data
This lab Cooperative + fixed time slices via time checks
loop forever:
  for each task:
    if now >= task.next:
       task.run()
       task.next += task.period

Step 1: Task table

package main

import (
    "fmt"
    "machine"
    "time"
)

type Task struct {
    Name   string
    Period time.Duration
    Next   time.Time
    Run    func()
}

func main() {
    machine.Serial.Configure(machine.UARTConfig{BaudRate: 115200})
    led := machine.LED
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})

    var blinkOn bool
    var ticks int

    tasks := []Task{
        {
            Name:   "blink",
            Period: 500 * time.Millisecond,
            Run: func() {
                blinkOn = !blinkOn
                led.Set(blinkOn)
            },
        },
        {
            Name:   "heartbeat",
            Period: 1 * time.Second,
            Run: func() {
                ticks++
                fmt.Fprintf(machine.Serial, "hb ticks=%d\n", ticks)
            },
        },
        {
            Name:   "sensor",
            Period: 100 * time.Millisecond,
            Run: func() {
                // pretend sample
                _ = time.Now().UnixNano() % 100
            },
        },
    }

    now := time.Now()
    for i := range tasks {
        tasks[i].Next = now.Add(tasks[i].Period)
    }

    for {
        now = time.Now()
        for i := range tasks {
            t := &tasks[i]
            if !now.Before(t.Next) {
                t.Run()
                t.Next = t.Next.Add(t.Period)
                // if still behind, skip ahead
                if now.After(t.Next) {
                    t.Next = now.Add(t.Period)
                }
            }
        }
        time.Sleep(1 * time.Millisecond) // cooperative idle
    }
}

Step 2: Inject a “bad” task

Add a task that time.Sleep(200 * time.Millisecond) occasionally. Observe heartbeat jitter. Lesson: cooperative scheduling fails open when a task hogs the CPU.

Step 3: Metrics

Track:

  • runs per task
  • late count when now.Sub(scheduled) > threshold
  • Print every 5 s over UART

Step 4: Design write-up

In NOTES.md:

  1. When cooperative is enough (UI blink, slow sensors).
  2. When you need interrupts/RTOS (motor control, strict deadlines).
  3. How TinyGo’s model maps to your board.

Learning Goals

  • Multi-rate task tables
  • Soft deadline tracking
  • Starvation under cooperative scheduling
  • Honest boundary vs FreeRTOS-class systems

Extensions

  1. Priority: run shortest period first each cycle.
  2. Disable a task via serial command.
  3. Port one task to pin interrupt (from Project 42).

Checkpoint

  • ≥3 tasks with different periods
  • UART shows heartbeat
  • Bad-task experiment documented
  • NOTES.md with RT conclusions