In the previous post, we got a build of the Go compiler tools running on the BeagleBone Black and wrote a simple “Hello, World!” program. Text is great, but one of the prominent new features of the BeagleBone Black over the original BeagleBone is the addition of a micro-HDMI port for graphics display, allowing the device to work as a full-fledged desktop system out of the box with no need for additional “capes”. Wouldn’t it be cool if we could draw some graphics through that port? Well, let’s do that.
There are quite a number of ways to display graphics on Linux systems and I’m likely to cover some more of these in future posts, but for now we’re going to stick with the simple dumb framebuffer interface exposed by Linux’s fbdev. fbdev has long been supported across a wide array of Linux devices, it is conceptually simple and best of all it can be programmed against using a memory-mapped device file which lets us easily utilize it from pure Go code without having to resort to CGO bindings.
We start by implementing a FrameBuffer struct type with some simple methods for drawing and just two members, one to hold a “backing” Go image.RGBA which will be used for client code to draw on, and the other a simple in-memory data buffer that represents the memory-mapped contents of the /dev/fb0 file. When the Flush method is called on the FrameBuffer, the contents of the backing image will be drawn to the memory-mapped framebuffer file, converting the RGBA data of the image to RGB555 to match the native framebuffer layout of the BeagleBone Black. The pixel conversion code isn’t particularly efficient, there are lots of ways it could be enhanced at the cost of readability (if you’re interested in the nitty gritty of really fast software blitting, I highly recommend checking out the C code of SDL or Enlightenment), but it is good enough for now.
A lot of the code below is simply Go definitions for things that are defined in /usr/include/linux/fb.h for C/C++ development. Since we’re ultimately calling system calls which expect pointers to structures with a specific alignment of members, it is important to ensure the Go struct type matches the C/C++ struct in terms of sizes for all the members. In this simple introduction we don’t really do much with the fbinfo data made available to us, though it is nice to have for future enhancements.
framebuffer.go
————–
package main
import (
"fmt"
"image"
"image/draw"
"os"
"syscall"
"unsafe"
)
// should match value of FBIOGET_VSCREENINFO in /usr/include/linux/fb.h
const fbioget_VSCREENINFO = 0x4600
// should match layout of fb_bitfield struct as defined in /usr/include/linux/fb.h
type bitfield struct {
offset uint
length uint
msbRight uint
}
// should match layout of fb_var_screeninfo struct as defined
// in /usr/include/linux/fb.h
type screenInfo struct {
width uint
height uint
widthVirtual uint
heightVirtual uint
offsetX uint
offsetY uint
bitsPerPixel uint
grayscale uint
red bitfield
green bitfield
blue bitfield
transp bitfield
nonstd uint
activate uint
heightMM uint
widthMM uint
accelFlags uint
pixclock uint
leftMargin uint
rightMargin uint
upperMargin uint
lowerMargin uint
hsyncLen uint
vsyncLen uint
sync uint
vmode uint
rotate uint
reserved [5]uint
}
type FrameBuffer struct {
img *image.RGBA
data []byte
}
func OpenFrameBuffer(devName string) (fb *FrameBuffer, err error) {
var file *os.File
if file, err = os.OpenFile(devName, os.O_RDWR, 0660); err != nil {
return
}
defer file.Close()
fb = new(FrameBuffer)
var fbInfo screenInfo
var errnop syscall.Errno
if _, _, errnop = syscall.Syscall(syscall.SYS_IOCTL, uintptr(file.Fd()),
uintptr(fbioget_VSCREENINFO),
uintptr(unsafe.Pointer(&fbInfo))); errnop != 0 {
err = fmt.Errorf(
"Unable to read framebuffer screeninfo (err %v).", errnop)
return
}
if fb.data, err = syscall.Mmap(int(file.Fd()), 0,
int(fbInfo.width*fbInfo.height*
(fbInfo.bitsPerPixel/8)),
syscall.PROT_WRITE, syscall.MAP_SHARED); err != nil {
return
}
// create RGBA backing image...
fb.img = image.NewRGBA(image.Rectangle{image.Point{0, 0},
image.Point{int(fbInfo.width), int(fbInfo.height)}})
return
}
func (fb *FrameBuffer) Width() int {
return fb.img.Bounds().Dx()
}
func (fb *FrameBuffer) Height() int {
return fb.img.Bounds().Dy()
}
func (fb *FrameBuffer) DrawImage(src image.Image, r image.Rectangle) {
draw.Draw(fb.img, r, src, image.ZP, draw.Src)
}
func (fb *FrameBuffer) Flush() {
// write current contents of our backing image.Image
// to the memory our framebuffer file is mapped to,
// convering 32-bit RGBA layout to 16-bit RGB555
data16 := *(*[]uint16)(unsafe.Pointer(&fb.data))
img32 := *(*[]uint32)(unsafe.Pointer(&fb.img.Pix))
dst := 0
width := uint(fb.img.Bounds().Dx())
height := uint(fb.img.Bounds().Dy())
for y := uint(0); y < height; y++ {
for x := uint(0); x < width; x++ {
data16[dst] = uint16((img32[dst]>>3)<<11 |
((img32[dst]>>8)&0xFF>>2)<<5 |
(img32[dst]>>16)&0xFF>>3)
dst++
}
}
}
And now let’s add some code to utilize this FrameBuffer struct:
main.go
——-
package main
import (
"image"
"image/color"
"image/png"
"log"
"net/http"
"time"
)
func main() {
bburl := "http://beagleboard.org/static/beaglebone/a3/Docs/beagle.png"
gourl := "http://golang.org/doc/gopher/bumper480x270.png"
var (
fb *FrameBuffer
err error
)
if fb, err = OpenFrameBuffer("/dev/fb0"); err != nil {
log.Fatalf("err: %v\n", err)
}
// Draw solid red to the framebuffer
drawBackground(fb, 255, 0, 0)
time.Sleep(3 * time.Second)
// Draw solid green to the framebuffer
drawBackground(fb, 0, 255, 0)
time.Sleep(3 * time.Second)
// Draw solid blue to the framebuffer
drawBackground(fb, 0, 0, 255)
time.Sleep(3 * time.Second)
// Draw Beaglebone image
if err = drawUrl(fb, bburl); err != nil {
log.Fatalf("err: %v\n", err)
}
time.Sleep(3 * time.Second)
// Draw solid green to the framebuffer
drawBackground(fb, 0, 255, 0)
// Draw Go image
if err = drawUrl(fb, gourl); err != nil {
log.Fatalf("err: %v\n", err)
}
time.Sleep(3 * time.Second)
// Draw solid red to the framebuffer
drawBackground(fb, 255, 0, 0)
}
func drawBackground(fb *FrameBuffer, r, g, b uint8) {
fb.DrawImage(&image.Uniform{color.RGBA{r, g, b, 255}},
image.Rect(0, 0, fb.Width(), fb.Height()))
fb.Flush()
}
func drawUrl(fb *FrameBuffer, url string) (err error) {
var (
resp *http.Response
img image.Image
)
if resp, err = http.Get(url); err != nil {
return
}
defer resp.Body.Close()
if img, err = png.Decode(resp.Body); err != nil {
return
}
// draw retrieved image, centered in the framebuffer
imgWidth, imgHeight := img.Bounds().Dx(), img.Bounds().Dy()
drawX, drawY := (fb.Width()-imgWidth)/2, (fb.Height()-imgHeight)/2
fb.DrawImage(img, image.Rect(drawX, drawY,
drawX+imgWidth, drawY+imgHeight))
fb.Flush()
return
}
Place the contents of both of these into files in the same directory, “go build” in that directory and you should end up with a program that will draw to the framebuffer, first a blank red screen, a blank green screen, a blank blue screen, followed by the BeagleBone logo image (on blue) and a Go logo image (on green), and then back to a blank red screen. To actually see this when you run it your BeagleBone will need to be attached to a display monitor via the micro-HDMI port. You’ll also want to shut down X11 (which runs by default on the BeagleBone Angstrom distro), you can do that by typing, as root:
# systemctl stop gdm.service
After executing that you should see your BeagleBone’s display switch from X11 to a standard tty-style text console. You may also want to shut down the cursor blinking so as not to be fighting that when our program draws to the framebuffer. You can do that by executing, at root:
# echo 0 > /sys/class/graphics/fbcon/cursor_blink
In the video below (apologies for the dark “HackerCam”) I have a BeagleBone Black hooked up to a Motorola Lapdock (bought for $45 back when they were being liquidated, quite nice!) as a dumb HDMI display device, it shows what you should expect to see upon running the code presented here.
