Skip to content

Commit 2951d88

Browse files
committed
feat(rp2350): add SST (Super Simple Tasker) scheduler for crea8
Adds an SST-style run-to-completion scheduler based on Quantum Leaps' Super Simple Tasker concepts. Key features: - Run-to-completion (RTC) task model - tasks cannot block - Single shared stack - minimal memory footprint (~4KB total) - Event-driven with lock-free event posting from ISRs - Priority-based preemption (higher priority tasks preempt) - Timer support for periodic events - Deterministic timing for hard real-time applications SST vs traditional threading: - Tasks run to completion (no blocking/sleeping within handler) - All tasks share one stack (vs per-task stacks) - Events posted to task queues trigger execution - Ideal for stepper motor control, 3D printing, PNP machines New files: - src/runtime/scheduler_sst.go - Core SST scheduler - src/runtime/runtime_rp2_sst.go - RP2350 SST runtime - src/runtime/gc_stack_sst.go - GC support for SST - src/internal/task/task_sst.go - Task stubs for SST - targets/crea8-sst.json - SST target configuration - src/examples/crea8-sst/ - Example application Build with: tinygo build -target=crea8-sst
1 parent 5815c9f commit 2951d88

File tree

7 files changed

+1086
-1
lines changed

7 files changed

+1086
-1
lines changed

compileopts/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
var (
1111
validBuildModeOptions = []string{"default", "c-shared", "wasi-legacy"}
1212
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise", "boehm"}
13-
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores", "preemptive"}
13+
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores", "preemptive", "sst"}
1414
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
1515
validPrintSizeOptions = []string{"none", "short", "full", "html"}
1616
validPanicStrategyOptions = []string{"print", "trap"}

src/examples/crea8-sst/main.go

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
// Example demonstrating SST (Super Simple Tasker) on RP2350 (Crea8 board).
2+
//
3+
// Build with:
4+
// tinygo build -target=crea8-sst -o firmware.uf2 ./examples/crea8-sst
5+
//
6+
// This example shows:
7+
// - Run-to-completion task model
8+
// - Event-driven programming
9+
// - Priority-based preemption
10+
// - Timer-based periodic events
11+
// - Single shared stack (minimal memory)
12+
//
13+
// SST is ideal for hard real-time applications like stepper motor control.
14+
15+
package main
16+
17+
import (
18+
"machine"
19+
"runtime"
20+
)
21+
22+
// Event signals (user signals start at SIG_USER)
23+
const (
24+
SIG_STEP = runtime.SIG_USER + iota // Stepper step event
25+
SIG_TEMP_READ // Temperature read event
26+
SIG_SERIAL_RX // Serial receive event
27+
SIG_DISPLAY // Display update event
28+
SIG_HEARTBEAT // LED heartbeat event
29+
)
30+
31+
// Task priorities (0 = highest)
32+
const (
33+
PRIO_STEPPER = 0 // Highest - stepper timing critical
34+
PRIO_SERIAL = 1 // High - responsive serial handling
35+
PRIO_TEMP = 2 // Medium - temperature control
36+
PRIO_DISPLAY = 3 // Lower - display updates
37+
PRIO_HEARTBEAT = 4 // Lowest - LED blink
38+
)
39+
40+
// Task state (shared - SST tasks can access directly since no preemption within task)
41+
var (
42+
stepperX int32
43+
stepperY int32
44+
stepperZ int32
45+
stepperE int32
46+
47+
targetX int32 = 1000
48+
targetY int32 = 1000
49+
50+
hotendTemp int16 = 25
51+
bedTemp int16 = 25
52+
targetHotend int16 = 200
53+
targetBed int16 = 60
54+
55+
ledState bool
56+
)
57+
58+
// SST Tasks
59+
var (
60+
stepperTask *runtime.SSTTask
61+
serialTask *runtime.SSTTask
62+
tempTask *runtime.SSTTask
63+
displayTask *runtime.SSTTask
64+
heartbeatTask *runtime.SSTTask
65+
)
66+
67+
func main() {
68+
// Configure pins
69+
machine.LED.Configure(machine.PinConfig{Mode: machine.PinOutput})
70+
machine.STEPPER_X_STEP.Configure(machine.PinConfig{Mode: machine.PinOutput})
71+
machine.STEPPER_X_DIR.Configure(machine.PinConfig{Mode: machine.PinOutput})
72+
machine.STEPPER_Y_STEP.Configure(machine.PinConfig{Mode: machine.PinOutput})
73+
machine.STEPPER_Y_DIR.Configure(machine.PinConfig{Mode: machine.PinOutput})
74+
machine.HEATER_HOTEND.Configure(machine.PinConfig{Mode: machine.PinOutput})
75+
machine.HEATER_BED.Configure(machine.PinConfig{Mode: machine.PinOutput})
76+
77+
// Set tick rate (10kHz for 100us resolution - good for steppers)
78+
runtime.SSTSetTickRate(10000)
79+
80+
println("Crea8 SST Demo")
81+
println("==============")
82+
println("Tick rate:", runtime.SSTGetTickRate(), "Hz")
83+
84+
// Create tasks (priority, handler)
85+
stepperTask = runtime.SSTTaskCreate(PRIO_STEPPER, stepperHandler)
86+
serialTask = runtime.SSTTaskCreate(PRIO_SERIAL, serialHandler)
87+
tempTask = runtime.SSTTaskCreate(PRIO_TEMP, tempHandler)
88+
displayTask = runtime.SSTTaskCreate(PRIO_DISPLAY, displayHandler)
89+
heartbeatTask = runtime.SSTTaskCreate(PRIO_HEARTBEAT, heartbeatHandler)
90+
91+
// Arm periodic timers
92+
// Stepper: every 1 tick (100us) for 10kHz step rate
93+
runtime.SSTTimerArm(stepperTask, SIG_STEP, 0, 1, 1)
94+
95+
// Temperature: every 1000 ticks (100ms)
96+
runtime.SSTTimerArm(tempTask, SIG_TEMP_READ, 0, 1000, 1000)
97+
98+
// Display: every 10000 ticks (1s)
99+
runtime.SSTTimerArm(displayTask, SIG_DISPLAY, 0, 10000, 10000)
100+
101+
// Heartbeat: every 5000 ticks (500ms)
102+
runtime.SSTTimerArm(heartbeatTask, SIG_HEARTBEAT, 0, 5000, 5000)
103+
104+
// Set idle callback
105+
runtime.SSTSetIdleCallback(idleCallback)
106+
107+
// The SST scheduler takes over from here
108+
// (run() is called automatically by the runtime)
109+
select {} // Never reached
110+
}
111+
112+
// stepperHandler - highest priority, runs to completion
113+
// Generates step pulses for motion control
114+
func stepperHandler(e *runtime.Event) {
115+
switch e.Signal {
116+
case runtime.SIG_INIT:
117+
println("Stepper task initialized")
118+
119+
case SIG_STEP:
120+
// Generate step pulses if we need to move
121+
stepped := false
122+
123+
if stepperX < targetX {
124+
machine.STEPPER_X_DIR.High()
125+
machine.STEPPER_X_STEP.High()
126+
stepperX++
127+
stepped = true
128+
} else if stepperX > targetX {
129+
machine.STEPPER_X_DIR.Low()
130+
machine.STEPPER_X_STEP.High()
131+
stepperX--
132+
stepped = true
133+
}
134+
135+
if stepperY < targetY {
136+
machine.STEPPER_Y_DIR.High()
137+
machine.STEPPER_Y_STEP.High()
138+
stepperY++
139+
stepped = true
140+
} else if stepperY > targetY {
141+
machine.STEPPER_Y_DIR.Low()
142+
machine.STEPPER_Y_STEP.High()
143+
stepperY--
144+
stepped = true
145+
}
146+
147+
// Small delay for pulse width, then lower step pins
148+
if stepped {
149+
// Inline delay (~1-2us)
150+
for i := 0; i < 10; i++ {
151+
// Busy loop for minimum pulse width
152+
}
153+
machine.STEPPER_X_STEP.Low()
154+
machine.STEPPER_Y_STEP.Low()
155+
}
156+
}
157+
// Task returns here (run-to-completion)
158+
}
159+
160+
// serialHandler - handles serial communication
161+
func serialHandler(e *runtime.Event) {
162+
switch e.Signal {
163+
case runtime.SIG_INIT:
164+
println("Serial task initialized")
165+
// Setup serial RX interrupt to post events
166+
// (simplified - real implementation would use UART IRQ)
167+
168+
case SIG_SERIAL_RX:
169+
// Process received data
170+
buf := make([]byte, 64)
171+
n, _ := machine.Serial.Read(buf)
172+
if n > 0 {
173+
processGCode(buf[:n])
174+
}
175+
}
176+
}
177+
178+
// tempHandler - temperature control
179+
func tempHandler(e *runtime.Event) {
180+
switch e.Signal {
181+
case runtime.SIG_INIT:
182+
println("Temperature task initialized")
183+
184+
case SIG_TEMP_READ:
185+
// Read temperatures (simplified - would use ADC)
186+
hotendTemp = readADCTemp(machine.TEMP_HOTEND_ADC)
187+
bedTemp = readADCTemp(machine.TEMP_BED_ADC)
188+
189+
// Bang-bang control (real implementation would use PID)
190+
if hotendTemp < targetHotend-2 {
191+
machine.HEATER_HOTEND.High()
192+
} else if hotendTemp > targetHotend+2 {
193+
machine.HEATER_HOTEND.Low()
194+
}
195+
196+
if bedTemp < targetBed-2 {
197+
machine.HEATER_BED.High()
198+
} else if bedTemp > targetBed+2 {
199+
machine.HEATER_BED.Low()
200+
}
201+
}
202+
}
203+
204+
// displayHandler - display/status updates
205+
func displayHandler(e *runtime.Event) {
206+
switch e.Signal {
207+
case runtime.SIG_INIT:
208+
println("Display task initialized")
209+
210+
case SIG_DISPLAY:
211+
println("Position X:", stepperX, "Y:", stepperY)
212+
println("Temp H:", hotendTemp, "/", targetHotend, "B:", bedTemp, "/", targetBed)
213+
println("Tick:", runtime.SSTGetTickCount())
214+
println("---")
215+
}
216+
}
217+
218+
// heartbeatHandler - LED blink to show system is alive
219+
func heartbeatHandler(e *runtime.Event) {
220+
switch e.Signal {
221+
case runtime.SIG_INIT:
222+
println("Heartbeat task initialized")
223+
224+
case SIG_HEARTBEAT:
225+
ledState = !ledState
226+
if ledState {
227+
machine.LED.High()
228+
} else {
229+
machine.LED.Low()
230+
}
231+
}
232+
}
233+
234+
// idleCallback - called when no tasks have pending events
235+
func idleCallback() {
236+
// Check for serial data and post event if available
237+
if machine.Serial.Buffered() > 0 {
238+
event := runtime.Event{Signal: SIG_SERIAL_RX}
239+
serialTask.Post(&event)
240+
}
241+
242+
// Could enter low-power mode here
243+
// arm.Asm("wfe")
244+
}
245+
246+
// processGCode processes G-code commands (simplified)
247+
func processGCode(data []byte) {
248+
if len(data) == 0 {
249+
return
250+
}
251+
252+
switch data[0] {
253+
case 'G':
254+
// Movement commands
255+
if len(data) > 1 {
256+
switch data[1] {
257+
case '0', '1': // G0/G1 - linear move
258+
// Parse X, Y, Z, E parameters (simplified)
259+
println("G-code move command")
260+
targetX += 100
261+
targetY += 100
262+
case '2', '8': // G28 - home
263+
println("G-code home command")
264+
targetX = 0
265+
targetY = 0
266+
}
267+
}
268+
269+
case 'M':
270+
// Machine commands
271+
if len(data) > 1 {
272+
switch data[1] {
273+
case '1': // M104/M109 - set hotend temp
274+
println("M-code hotend temp")
275+
targetHotend = 200
276+
case '4': // M140 - set bed temp
277+
println("M-code bed temp")
278+
targetBed = 60
279+
}
280+
}
281+
}
282+
}
283+
284+
// readADCTemp reads temperature from ADC pin (simplified)
285+
func readADCTemp(pin machine.Pin) int16 {
286+
// Simplified - real implementation would use ADC
287+
return 25 // Room temperature
288+
}
289+
290+
// PostStepEvent can be called from ISR to trigger step
291+
func PostStepEvent() {
292+
stepperTask.PostFromISR(SIG_STEP, 0)
293+
}

src/internal/task/task_sst.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//go:build scheduler.sst
2+
3+
// SST doesn't use the traditional task model.
4+
// This file provides stubs to satisfy the runtime interface.
5+
6+
package task
7+
8+
import "unsafe"
9+
10+
// Task is unused in SST but needed for interface compatibility
11+
type Task struct {
12+
Next *Task
13+
Ptr unsafe.Pointer
14+
Data uint64
15+
RunState uint8
16+
}
17+
18+
// Stub constants
19+
const (
20+
RunStatePaused = 0
21+
RunStateRunning = 1
22+
RunStateResuming = 2
23+
)
24+
25+
// Current returns nil in SST (no traditional tasks)
26+
func Current() *Task {
27+
return nil
28+
}
29+
30+
// Pause is a no-op in SST
31+
func Pause() {
32+
}
33+
34+
// PauseLocked is a no-op in SST
35+
func PauseLocked() {
36+
}
37+
38+
// OnSystemStack returns true in SST (single stack)
39+
func OnSystemStack() bool {
40+
return true
41+
}
42+
43+
// SystemStack returns the system stack pointer
44+
func SystemStack() uintptr {
45+
return 0
46+
}

src/runtime/gc_stack_sst.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//go:build scheduler.sst
2+
3+
// GC stack scanning for SST scheduler.
4+
// SST uses a single shared stack, making GC simpler.
5+
6+
package runtime
7+
8+
// gcMarkReachable scans the single shared stack
9+
func gcMarkReachable() {
10+
// Scan the current (only) stack
11+
scanCurrentStack()
12+
13+
// Scan globals
14+
findGlobals(markRoots)
15+
}
16+
17+
//go:export tinygo_scanCurrentStack
18+
func scanCurrentStack()
19+
20+
//go:export tinygo_scanstack
21+
func scanstack(sp uintptr) {
22+
// Single stack - scan from sp to top
23+
markRoots(sp, stackTop)
24+
}
25+
26+
// gcResumeWorld is a no-op for SST (single core, single stack)
27+
func gcResumeWorld() {
28+
// Nothing to do
29+
}

0 commit comments

Comments
 (0)