Introduction: Waveshare EPaper and a RaspberryPi

I'm a display nerd, I know. So I got this Waveshare ePaper 2.9" display from ama...n and it was a little nasty to adopt the software, so here is how it went for me.

What to expect:

Some python code for the raspberry to run this display.

  • uses good old PythonImagingLibrary
    • can load any Image
    • allows image manipulation and drawing
  • can fetch images from the internet
  • demo code shows some search results from


Here is the waveshare Wiki page with the somewhat useful infos you may need. Unfortunately I am missing info about the memory layout just to know where I can expect to write a bit for a certain pixel. It also lacks timing infos, the code does a lot of delay's and there is a busy pin to read from but when and how I use which ??? The code also contains initialisation code that is copied to the display and you will get no glue what it is. And no the PDF document about the display does not reveal this either.

After some experimentation, I can get a refresh rate of about 2fps for a complete image refresh, with the partial update function. The pixels do not completely reset, I guess you need to do the long refresh cycle which inverts everything a view times to get completely clean display back.

Connections to Raspberry

I used a Pi3 but it should work with any.

Search the web for "Raspberry pinout" e.g. this one and match with the waveshare doc, or simply use this table..

e-Paper  RaspberryPi
3.3V 3.3V (pin1)
GND GND (pin6)
DIN MOSI (pin19)
CLK SCLK (pin23)
CS CE0 (pin24)
DC BCM25(pin22)
RST BCM17(pin11)
BUSY BCM24(pin18)

The Code

There is the which runs the fancy stuff and there is which is a stripped down version of the interfacing code provided py waveshare. I thrown out almost everything which did not just copy an image to the display - it.. eh.. well they tried, but putting the frame together is much easier done on the Pi and the copied over with a single command.

Just download the attached Then run the code with "python". You will need to install at least PIL and GPIO phyton libs like this.

sudo apt-get install python-requests python-pil python-rpi.gpio

Also make sure the SPI is enabled, edit /boot/config.txt and uncomment the line

dtparam=spi=on - this does init the display, gets some data from a duckduckgo search and displays that along with the current time. (instructables messed up the code, use the attached zip file)

#!/usr/bin/python<br>import spidev as SPI   # where the display connects

import Image, ImageDraw, ImageFont  # PIL - PythonImageLibrary

import time, datetime, sys, signal, urllib, requests
from EPD_driver import EPD_driver
def handler(signum, frame):
    print 'SIGTERM'
signal.signal(signal.SIGTERM, handler)
bus = 0 
device = 0
disp = EPD_driver(spi = SPI.SpiDev(bus, device))
print "disp size : %dx%d"%(disp.xDot, disp.yDot)
print '------------init and Clear full screen------------'
# display part
imagenames = [] 
search = ""
if search:
    req = requests.get(search)
    if req.status_code == 200:
        for topic in req.json()["RelatedTopics"]:
            if "Topics" in topic:
                for topic2 in topic["Topics"]:
                        url = topic2["Icon"]["URL"]
                        text = topic2["Text"]
                        if url:
                            imagenames.append( (url,text) )
                        # print topic
                url = topic["Icon"]["URL"]
                if url:
                    imagenames.append( url )
                # print topic
        print req.status_code
# font for drawing within PIL
myfont10 = ImageFont.truetype("amiga_forever/amiga4ever.ttf", 8)
myfont28 = ImageFont.truetype("amiga_forever/amiga4ever.ttf", 28)
# mainimg is used as screen buffer, all image composing/drawing is done in PIL,
# the mainimg is then copied to the display (drawing on the disp itself is no fun)
mainimg ="1", (296,128))
name = ("images/downloaded.png", "bla")
skip = 0
while 1:
    for name2 in imagenames:
        print '---------------------'
        skip = (skip+1)%7
            starttime = time.time()
            if skip==0 and name2[0].startswith("http"):
                name = name2
                urllib.urlretrieve(name[0], "images/downloaded.png")
                name = ("images/downloaded.png", name2[1])
            im =[0])
            print name, im.format, im.size, im.mode
            im = im.convert("1") #, dither=Image.NONE)
            # print 'thumbnail', im.format, im.size, im.mode
            loadtime = time.time()
            print 't:load+resize:', (loadtime - starttime)
            draw = ImageDraw.Draw(mainimg)
            # clear
            draw.rectangle([0,0,296,128], fill=255)
            # copy to mainimg
            ypos = (disp.xDot - im.size[1])/2
            xpos = (disp.yDot - im.size[0])/2
            print 'ypos:', ypos, 'xpos:', xpos
            mainimg.paste(im, (xpos,ypos))
            # draw info text
            ts = draw.textsize(name[1], font=myfont10)
            tsy = ts[1]+1
            oldy = -1
            divs = ts[0]/250
            for y in range(0, divs):
                newtext = name[1][(oldy+1)*len(name[1])/divs:(y+1)*len(name[1])/divs]
                # print divs, oldy, y, newtext
                oldy = y
                draw.text((1, 1+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((1, 3+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((3, 3+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((3, 1+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((2, 2+y*tsy), newtext, fill=0, font=myfont10)
            #draw time
            now =
            tstr = "%02d:%02d:%02d"%(now.hour,now.minute,now.second)
            # draw a shadow, time
            tpx = 36
            tpy = 96
            for i in range(tpy-4, tpy+32, 2):
                draw.line([0, i, 295, i], fill=255)
            draw.text((tpx-1, tpy  ), tstr, fill=0, font=myfont28)
            draw.text((tpx-1, tpy-1), tstr, fill=0, font=myfont28)
            draw.text((tpx  , tpy-1), tstr, fill=0, font=myfont28)
            draw.text((tpx+2, tpy  ), tstr, fill=0, font=myfont28)
            draw.text((tpx+2, tpy+2), tstr, fill=0, font=myfont28)
            draw.text((tpx  , tpy+2), tstr, fill=0, font=myfont28)
            draw.text((tpx  , tpy  ), tstr, fill=255, font=myfont28)
            del draw
            im = mainimg.transpose(Image.ROTATE_90)
            drawtime = time.time()
            print 't:draw:', (drawtime - loadtime)
            listim = list(im.getdata())
            # print im.format, im.size, im.mode, len(listim)
            listim2 = []
            for y in range(0, im.size[1]):
                for x in range(0, im.size[0]/8):
                    val = 0
                    for x8 in range(0, 8):
                        if listim[(im.size[1]-y-1)*im.size[0] + x*8 + (7-x8)] > 128:
                            # print x,y,x8,'ON'
                            val = val | 0x01 << x8
                            # print x,y,x8,'OFF'
                    # print val
            for x in range(0,1000):
            # print len(listim2)
            convtime = time.time()
            print 't:conv:', (convtime - loadtime)
            ypos = 0
            xpos = 0
            disp.EPD_Dis_Part(xpos, xpos+im.size[0]-1, ypos, ypos+im.size[1]-1, listim2) # xStart, xEnd, yStart, yEnd, DisBuffer
            # disp.delay()
            uploadtime = time.time()
            print 't:upload:', (uploadtime - loadtime)
        except IOError as ex:
            print 'IOError', str(ex)</p>


I used the free font "Amiga Forever" by Freaky Fonts .

Images shown on the disp are search results from duckduckgo "cat" search, no preferences for whatever comes up there.


mariosis (author)2017-10-04

Hi, it's difficult to "fix" the software without the hardware for testing it, It took me a some iterations to get mine running.
However, the docu of the 2.13 HAT says something about a "virtual width" of 128 instead of 122, meaning the display memory is 128 pixel width to keep the lines byte aligned.
Maybe a width setting of 128 does the trick.


MauroC52 (author)2017-09-22

Hi, can I try with the 2.13 RPI HAT b/w version? Demo on waveshare wiki for that one don't work, I'm quite lost!!!


mariosis (author)MauroC522017-09-22

Hi, yes you can :)

The Waveshare Democode worked for me too, just not so easy to mod.
Connections look very much the same.
You need to adopt the screen resolution, just grep my code for "296" and "128" e.g. "grep -n 128 *" will show you all the place where to change stuff. Also the 2.9 display is a portrait mode disp, might be confusing, there is a "transpose" function in my code dealing with this.
Good Luck.

Mauro52 (author)mariosis2017-09-25

tnks a lot for answering!!!

I changed the Register initial variable for 2x13 screen in the

I manually changed the to match the new resolution (I don't know how to use the "grep" command) switching all "296" with "250" and "128" with "122"

this is the output when I run the script in the terminal:

pi@raspberrypi:~/Desktop/paperDisp2 $ python


Reset is complete

disp size : 122x250

------------init and Clear full screen------------

1.init full screen

2.clear full screen

search for Acorn#entries 11

search for Amiga#entries 7

search for DAC#entries 14

search for DAC#entries 14

^CTraceback (most recent call last):

File "", line 167, in <module>


File "", line 75, in imageToDisplay

val = val | 0x01 << x8


On the 2.13 inch epaper, image and time are not displayed correctly (just clear function works very well)

can you write something easier for python newbie?

e.g. something to display a local image for 3 seconds and after a counter.

I know that it's not "funny" for you, but python newbies like me could start playing....

btw tnks a lot

About This Instructable




More by mariosis:Waveshare EPaper and a RaspberryPiDaily Standup TimerVintage DAC for RaspberryPi audio
Add instructable to: