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.

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