Skip to content

Go routine loop with timer channel writing to I2C device freezes after a while #4975

@abulka

Description

@abulka

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.

Image

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()
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions