Skip to content

Instantly share code, notes, and snippets.

@riyaz-ali
Created March 14, 2020 18:21
Show Gist options
  • Save riyaz-ali/74c4328f3e3b53152b1e4a548255d00d to your computer and use it in GitHub Desktop.
Save riyaz-ali/74c4328f3e3b53152b1e4a548255d00d to your computer and use it in GitHub Desktop.
Waveshare 2.9-inch E-paper display driver in Golang

Waveshare E-paper

This gist contains driver for Waveshare's 2.9-inch E-paper display module.

Usage

// This examples runs on Raspberry Pi 3 board and uses "github.com/stianeikeland/go-rpio/v4" as GPIO library
// You can use any other library too as long as it satify the types defined in gpio.go
// To begin with:
// 1) initialize your board and gpio pins
// 2) create a new display driver instance and pass in the required pins
// 3) set the driver mode (partial vs. full update)
// 4) get creative and draw something!
// 5) put the display into deep sleep mode (0-power state)


func init() {
	//start the GPIO controller
	if err := rpio.Open(); err != nil {
		log.Fatalf("failed to start gpio: %v", err)
	}

	// Enable SPI on SPI0
	if err := rpio.SpiBegin(rpio.Spi0); err != nil {
		log.Fatalf("failed to enable SPI: %v", err)
	}

	// configure SPI settings
	rpio.SpiSpeed(4_000_000)
	rpio.SpiMode(0, 0)

	rpio.Pin(17).Mode(rpio.Output)
	rpio.Pin(25).Mode(rpio.Output)
	rpio.Pin(8).Mode(rpio.Output)
	rpio.Pin(24).Mode(rpio.Input)
}

func main() {
	defer rpio.Close()

	// initialize the driver
	var display = epd.New(rpio.Pin(17), rpio.Pin(25), rpio.Pin(8), ReadablePinPatch{rpio.Pin(24)}, rpio.SpiTransmit)
	display.Mode(epd.PartialUpdate)

	var img = gg.NewContext(display.Width, display.Height)
	img.SetColor(color.White)
	img.Clear()

	var cx, cy = float64(display.Width) / 2, float64(display.Height) / 2

	var s1 = "go get"
	var hs1, _ = img.MeasureString(s1)
	var s2 = "git.io/epd2in9"
	var hs2, ws2 = img.MeasureString(s2)

	img.SetColor(color.Black)
	img.DrawRectangle(cx-(hs2/2)-4, cy-(ws2/2)-6, hs2+8, ws2+6)
	img.Fill()

	img.SetColor(color.Black)
	img.DrawString(s1, cx-(hs1/2), cy-ws2-8)
	img.Stroke()

	img.SetColor(color.White)
	img.DrawString(s2, cx-(hs2/2), cy)
	img.Stroke()

	if e := display.Draw(img.Image()); e != nil {
		fmt.Printf("failed to draw: %v\n", e)
		display.Clear(color.White)
	}

	display.Sleep()
}

type ReadablePinPatch struct {
	rpio.Pin
}

func (pin ReadablePinPatch) Read() uint8 {
	return uint8(pin.Pin.Read())
}
// Package epd provides driver for Waveshare's E-paper e-ink display
package epd
import (
"errors"
"image"
"image/color"
"math"
"time"
)
// ErrInvalidImageSize is returned if the given image bounds doesn't fit into display bounds
var ErrInvalidImageSize = errors.New("invalid image size")
// LookupTable defines a type holding the instruction lookup table
// This lookup table is used by the device when performing refreshes
type Mode uint8
const (
FullUpdate Mode = iota
PartialUpdate
)
// fullUpdate is a lookup table used whilst in full update mode
var fullUpdate = []byte{
0x50, 0xAA, 0x55, 0xAA, 0x11, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x1F, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
// partialUpdate is a lookup table used whilst in partial update mode
var partialUpdate = []byte{
0x10, 0x18, 0x18, 0x08, 0x18, 0x18,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x13, 0x14, 0x44, 0x12,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
// EPD defines the base type for the epaper display driver
type EPD struct {
// dimensions of the display
Height int
Width int
// pins used by this driver
rst WriteablePin // for reset signal
dc WriteablePin // for data/command select signal; D=HIGH C=LOW
cs WriteablePin // for chip select signal; this pin is active low
busy ReadablePin // for reading in busy signal
// SPI transmitter
transmit Transmit
}
// New creates a new EPD device driver
func New(rst, dc, cs WriteablePin, busy ReadablePin, transmit Transmit) *EPD {
return &EPD{296, 128, rst, dc, cs, busy, transmit}
}
// reset resets the display back to defaults
func (epd *EPD) reset() {
epd.rst.High()
time.Sleep(200 * time.Millisecond)
epd.rst.Low()
time.Sleep(10 * time.Millisecond)
epd.rst.High()
time.Sleep(200 * time.Millisecond)
}
// command transmits single byte of command instruction over the SPI line
func (epd *EPD) command(c byte) {
epd.dc.Low()
epd.cs.Low()
epd.transmit(c)
epd.cs.High()
}
// data transmits single byte of data payload over SPI line
func (epd *EPD) data(d byte) {
epd.dc.High()
epd.cs.Low()
epd.transmit(d)
epd.cs.High()
}
// idle reads from busy line and waits for the device to get into idle state
func (epd *EPD) idle() {
for epd.busy.Read() == 0x1 {
time.Sleep(200 * time.Millisecond)
}
}
// mode sets the device's mode (based on the LookupTable)
// The device can either be in FullUpdate mode where the whole display is updated each time an image is rendered
// or in PartialUpdate mode where only the changed section is updated (and it doesn't cause any flicker)
//
// Waveshare recommends doing full update of the display at least once per-day to prevent ghost image problems
func (epd *EPD) Mode(mode Mode) {
epd.reset()
// command+data below is taken from the python sample driver
// DRIVER_OUTPUT_CONTROL
epd.command(0x01)
epd.data(byte((epd.Height - 1) & 0xFF))
epd.data(byte(((epd.Height - 1) >> 8) & 0xFF))
epd.data(0x00)
// BOOSTER_SOFT_START_CONTROL
epd.command(0x0C)
epd.data(0xD7)
epd.data(0xD6)
epd.data(0x9D)
// WRITE_VCOM_REGISTER
epd.command(0x2C)
epd.data(0xA8)
// SET_DUMMY_LINE_PERIOD
epd.command(0x3A)
epd.data(0x1A)
// SET_GATE_TIME
epd.command(0x3B)
epd.data(0x08)
// DATA_ENTRY_MODE_SETTING
epd.command(0x11)
epd.data(0x03)
// WRITE_LUT_REGISTER
epd.command(0x32)
var lut = fullUpdate
if mode == PartialUpdate {
lut = partialUpdate
}
for _, b := range lut {
epd.data(b)
}
}
// Sleep puts the device into "deep sleep" mode where it draws zero (0) current
//
// Waveshare recommends putting the device in "deep sleep" mode (or disconnect from power)
// if doesn't need updating/refreshing.
func (epd *EPD) Sleep() {
epd.command(0x10)
epd.data(0x01)
}
// turnOnDisplay activates the display and renders the image that's there in the device's RAM
func (epd *EPD) turnOnDisplay() {
epd.command(0x22)
epd.data(0xC4)
epd.command(0x20)
epd.command(0xFF)
epd.idle()
}
// window sets the window plane used by device when drawing the image in the buffer
func (epd *EPD) window(x0, x1 byte, y0, y1 uint16) {
epd.command(0x44)
epd.data((x0 >> 3) & 0xFF)
epd.data((x1 >> 3) & 0xFF)
epd.command(0x45)
epd.data(byte(y0 & 0xFF))
epd.data(byte((y0 >> 8) & 0xFF))
epd.data(byte(y1 & 0xFF))
epd.data(byte((y1 >> 8) & 0xFF))
}
// cursor sets the cursor position in the device window frame
func (epd *EPD) cursor(x uint8, y uint16) {
epd.command(0x4E)
epd.data((x >> 3) & 0xFF)
epd.command(0x4F)
epd.data(byte(y & 0xFF))
epd.data(byte((y >> 8) & 0xFF))
epd.idle()
}
// Clear clears the display and paints the whole display into c color
func (epd *EPD) Clear(c color.Color) {
var img = image.White
if c != color.White {
img = image.Black // anything other than white is treated as black
}
_ = epd.Draw(img)
}
// Draw renders the given image onto the display
func (epd *EPD) Draw(img image.Image) error {
var isvertical = img.Bounds().Size().X == epd.Width && img.Bounds().Size().Y == epd.Height
var _, uniform = img.(*image.Uniform) // special case for uniform images which have infinite bound
if !uniform && !isvertical {
return ErrInvalidImageSize
}
epd.window(0, byte(epd.Width-1), 0, uint16(epd.Height-1))
for i := 0; i < epd.Height; i++ {
epd.cursor(0, uint16(i))
epd.command(0x24) // WRITE_RAM
for j := 0; j < epd.Width; j += 8 {
// this loop converts individual pixels into a single byte
// 8-pixels at a time and then sends that byte to render
var b = 0xFF
for px := 0; px < 8; px++ {
var pixel = img.At(j+px, i)
if isdark(pixel.RGBA()) {
b &= ^(0x80 >> (px % 8))
}
}
epd.data(byte(b))
}
}
epd.turnOnDisplay()
return nil
}
// isdark is a utility method which returns true if the pixel color is considered dark else false
// this function is taken from https://git.io/JviWg
func isdark(r, g, b, _ uint32) bool {
return math.Sqrt(
0.299*math.Pow(float64(r), 2)+
0.587*math.Pow(float64(g), 2)+
0.114*math.Pow(float64(b), 2)) <= 130
}
package epd
// This file contains common interface and type definitions that the driver uses
// to abstract away the underlying GPIO mechanism
// This allows the driver to work with different GPIO libraries.
// This also allows the user of the package to perform initialization of the GPIO package according to their requirement
// and doesn't force the use of any pin or initialization scheme on them
//
// The interfaces here are modelled after the popular GPIO library for raspberry pi [github.com/stianeikeland/go-rpio/v4]
// WriteablePin is a GPIO pin through which the driver can write digital data
type WriteablePin interface {
// High sets the pins output to digital high
High()
// Low sets the pins output to digital low
Low()
}
// ReadablePin is a GPIO pin through which the driver can read digital data
type ReadablePin interface {
// Read reads from the pin and return the data as a byte
Read() uint8
}
// Transmit is a function that sends the data payload across to the device via the SPI line
type Transmit func(data ...byte)
@riyaz-ali
Copy link
Author

Moved to https://github.com/riyaz-ali/epd with MIT licensed source 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment