Giving Wall-e a screen

Wall-e wouldn’t be complete without a screen.  We’re using this  128×128 colour OLED screen that works with the PiOLED kernel driver.

After enabling SPI using raspi-config and wiring it up to the SPI bus and a couple of GPIOs needed to access its reset and data/command pins:

we were able to get it working with the fbtft driver, which exposes the screen as a standard framebuffer device.

Figuring out the colour map

I hadn’t worked with the framebuffer before but it turned out to be fairly simple to use.  Basically, it exposes the screen as a special type of file; if you open that file and write a couple of bytes to it, it updates a pixel on the screen and then moves the cursor to the next pixel.  Once you’ve written 128 pixels, it moves to the next line.  You can use the seek operation to move the cursor to a different place in the file, which is the same as moving the cursor to a different place on screen.

This particular screen supports 16-bit colour, with 5 bits for red, 6 bits for green and 5 for blue, so the process for writing a colour to the screen is something like this:

  • Calculate your red, green and blue intensity.
  • Scale red and blue to the range 0-31 (i.e. 5 bits of precision)
  • Scale green to 0-63 (i.e. 6 bits).
  • Pack the bits into a 16 bits: rrrrrggggggbbbbb and then break the 16-bits up into two bytes: rrrrrggg and gggbbbbb
  • Write those two bytes to the address of the pixel; first the gggbbbbb and then the rrrrrggg byte.

Since we’re writing our code in golang, I searched around for a golang drawing library and found the gg library.
As a prototype, I used that to draw a mock-up of Wall-e’s screen and then scanned the resulting gg Image, extracting the pixels and writing them to the frame buffer in the 16-bit format:

The code for the above looks like this:

func drawOnScreen() {
	// Open the frame buffer.
	f, err := os.OpenFile("/dev/fb1", os.O_RDWR, 0666)
	if err != nil {
		panic(err)
	}

	// Loop, simulating a change to battery charge every half second.
	charge := 0.0
	for range time.NewTicker(500 * time.Millisecond).C {
		// Create a drawing context of the right size
		const S = 128
		dc := gg.NewContext(S, S) 
		dc.SetRGBA(1, 0.9, 0, 1) // Yellow

		// Get the current heading
		headingLock.Lock()
		j := headingEstimate
		headingLock.Unlock()

		// Move the current origin over to the right.
		dc.Push()
		dc.Translate(60, 5)
		dc.DrawString("CHARGE LVL", 0, 10)

		// Draw the larger power bar at the bottom. Colour depends on charge level.
		if charge < 0.1 {
			dc.SetRGBA(1, 0.2, 0, 1)
			dc.Push()
			dc.Translate(14, 80)
			DrawWarnign(dc)
			dc.Pop()
		}

		dc.DrawRectangle(36, 70, 30, 10)

		for n := 2; n < 13; n++ { if charge >= (float64(n) / 13) {
				dc.DrawRectangle(38, 75-float64(n)*5, 26, 3)
			}
		}

		dc.Fill()

		dc.DrawString(fmt.Sprintf("%.1fv", 11.4+charge), 33, 93)

		dc.SetRGBA(1, 0.9, 0, 1)

		// Draw the compass
		dc.Translate(14, 30)
		dc.Rotate(gg.Radians(j))
		dc.Scale(0.5, 1.0)
		dc.DrawRegularPolygon(3, 0, 0, 14, 0)
		dc.Fill()

		dc.Pop()

		charge += 0.1
		if charge > 1 {
			charge = 0
		}

		// Copy the colours over to the frame buffer.
		var buf [128 * 128 * 2]byte
		for y := 0; y < S; y++ {
			for x := 0; x < S; x++ { c := dc.Image().At(x, y) r, g, b, _ := c.RGBA() // 16-bit pre-multiplied rb := byte(r >> (16 - 5))
				gb := byte(g >> (16 - 6)) // Green has 6 bits
				bb := byte(b >> (16 - 5))

				buf[(127-y)*2+(x)*128*2+1] = (rb << 3) | (gb >> 3)
				buf[(127-y)*2+(x)*128*2] = bb | (gb << 5)
			}
		}
		_, err = f.Seek(0, 0)
		if err != nil {
			panic(err)
		}

		lock.Lock()
		_, err = f.Write(buf[:])
		lock.Unlock()
		if err != nil {
			panic(err)
		}
	}
}

 

Leave a Reply

Your email address will not be published. Required fields are marked *