-
Notifications
You must be signed in to change notification settings - Fork 974
Description
A go routine looping and periodically calling draw(), which just does ClearDisplay() and Display() on an attached ssd1306 OLED display will freeze after a while.
In my particular repro code, I have:
- A second go routine looping, blocking on a timer channel and printing "tick" every 1s.
- The main loop also looping, blocking on a timer channel and printing "just ticking" every 30ms.
The freeze only happens if a real I2C device is connected - ssd1306 in my case.
The freeze does not happen if you disconnect the oled sd1306 display.
package main
import (
"machine"
"sync"
"time"
"tinygo.org/x/drivers/ssd1306"
)
type State struct {
running bool
dev ssd1306.Device
ch chan int
}
func main() {
i2c := machine.I2C0
i2c.Configure(machine.I2CConfig{
Frequency: 400000,
SDA: machine.GP0,
SCL: machine.GP1,
})
dev := ssd1306.NewI2C(i2c)
dev.Configure(ssd1306.Config{
Address: 0x3C,
Width: 128,
Height: 32,
})
state := &State{
running: true,
dev: dev,
ch: make(chan int),
}
var wg sync.WaitGroup
wg.Add(1)
go state.run(&wg)
time.Sleep(1 * time.Second)
go func() {
for state.running {
state.draw()
delay := 50*time.Millisecond
print("d", delay.Milliseconds(), "ms\n")
time.Sleep(delay)
}
}()
time.Sleep(1 * time.Second)
// === MAIN LOOP ===
next := 30 * time.Millisecond // even 130ms freezes eventually
var timer *time.Timer = time.NewTimer(next)
for state.running {
timer.Stop()
timer.Reset(next)
<-timer.C // Block until timer fires
println("just ticking")
time.Sleep(1 * time.Millisecond) // adding sleep makes freeze happen much sooner
}
wg.Wait()
}
func (s *State) draw() {
s.dev.ClearDisplay()
s.dev.Display()
}
func (s *State) run(wg *sync.WaitGroup) {
defer wg.Done()
var timer *time.Timer
var timerC <-chan time.Time
for s.running {
next := 1000 * time.Millisecond
if timer == nil {
timer = time.NewTimer(next)
timerC = timer.C
} else {
timer.Stop()
timer.Reset(next)
}
select {
case <-s.ch:
println("got value on channel (never sent)")
case <-timerC:
println("tick")
}
time.Sleep(1 * time.Millisecond)
}
}
Run with:
tinygo flash -target=pico --monitor ./cmd-demos/bug-oled
Output:
% tinygo flash -target=pico --monitor ./cmd-demos/bug-oled
Connected to /dev/cu.usbmodem101. Press Ctrl-C to exit.
tick
d50ms
d50ms
tick
just ticking
just ticking
d50ms
just ticking
just ticking
...
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
d50ms
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
just ticking
d50ms
p
Freeze.
Observations
Replacing the main loop with the following code avoids the freeze:
next := 30 * time.Millisecond
for state.running {
println("just ticking")
time.Sleep(next)
}
However there are good reasons why I am using timer channels and not just simple for loops with sleep like this.
The repro example is extracted from a bigger application which is why it isn't a 'simple' example.
Other observations:
Slowing down the main loop with a 130ms timer instead of 30ms causes the freeze to happen later, but it still happens.
Adding a time.Sleep(1 * time.Millisecond)
after the println("just ticking")
in the main loop makes the freeze happen much sooner.
Attaching a SSD1306 display
Its easy to attach a SSD1306 display to the Pi Pico, just connect the I2C pins SDA to GP0, SCL to GP1, VCC to 3.3V and GND to GND.

If you want something to see on the OLED before the freeze, you can modify the draw()
function to do something like this:
import (
"image/color"
...
)
func (s *State) draw() {
s.dev.ClearDisplay()
// Actually drawing something doesn't change the freeze behavior
ColorWhite := color.RGBA{255, 255, 255, 255}
s.dev.FillRectangle(0, 0, 10, 10, ColorWhite)
time.Sleep(300 * time.Millisecond)
s.dev.Display()
}