How to Use an E-ink Display with Raspberry Pi

If you click our links and make a purchase, we may earn an affiliate commission. Learn more

I’ve always been fascinated by projects that use that paper display (you know, like the Kindle screen), so I decided to give it a shot. Since it wasn’t as easy as I’d hoped to get started, I’ve documented all the steps. This way, you can get a head start when you decide to try it yourself.

An e-ink display is a screen that mimics the look of printed paper, uses very low power, and remains readable even in bright sunlight. With a Raspberry Pi, it’s often used for static display or tiny dashboards that don’t need constant refresh.

In this tutorial, I’m sharing my progress to give you some guidance. I’ll start by going over the basics: what hardware you’ll need, how to set it up, and the first tests. I’m no expert, but I hope that helps you get started faster than I did.

If you’re new to Raspberry Pi or Linux, I’ve got something that can help you right away!
Download my free Linux commands cheat sheet – it’s a quick reference guide with all the essential commands you’ll need to get things done on your Raspberry Pi. Click here to get it for free!

What Is an E-Ink Display (and Why Use One)?

E-ink (or “electronic ink”) is a display technology that looks like printed paper. You probably have already seen it used on Kindle devices or similar ebook readers. Instead of emitting light like a regular monitor, it reflects ambient light, making it very comfortable to read.

In the Raspberry Pi ecosystem, the main benefit of e-ink is it uses very little power. In fact, the image actually stays on the screen even when the power is off. It only uses energy when you update the display.

So, if you’re thinking of a badge with your name and company logo, or an outdoor project that needs to run on battery power and display a few data points while staying readable in direct sunlight, this is the perfect use case.

This is not plugged into anything, and doesn’t need power to stay readable.

There are a few limitations, like slow refresh rates (don’t expect to play animations or videos on it) or the quality and colors (most models are black and white), but it’s pretty cool to use for tiny dashboards, weather stations, price tags or notifications.

If you’ve got a project that needs to show data and the content doesn’t change often, but you need it to stay visible no matter what, this could work. It’s also fun to use, not too expensive, and easy to integrate into any Python project.

Hardware Requirements to Use an E-Ink Display on Raspberry Pi

You don’t need much to get started, except a Raspberry Pi and the display HAT itself, but let’s go over all the requirements quickly before showing you some technical stuff.

Something not working as expected?
You can get answers from real experts in minutes.
Get help with your setup

By the way, HAT stands for “Hardware Attached on Top” and is basically an extension board to add new features easily to your Raspberry Pi.

What you need

There are many brands and models you can use for that kind of setup, but here’s what I’m using. I think it’s a pretty affordable setup. If you don’t have any other constraints, it’s probably safe to start with the same stuff.

  • An E-Ink Display Hat: I’m using the 2.13″ version from Waveshare, which seems to be the industry standard in this category. There are a few other sizes available, so you might want to try something bigger based on your needs (or Raspberry Pi model), but the technical steps should be pretty similar.
  • A Raspberry Pi 4: At first, I wanted to use it with a Pi Zero 2, which is the same size, but it didn’t fit well with my case, so I upgraded to a Pi 4. But any model should do. It doesn’t need a lot of resources to run anyway.

Other than that, you only need the basics: a decent microSD card and a reliable power supply, eventually a monitor and keyboard/mouse, but you can do everything headless if you prefer.

Set up your Raspberry Pi

Before you write your first line of Python code, you’ll need to plug it in and set up your system. Here is what you need to do.

Wiring the HAT

My display HAT was provided with GPIO wires, which are useful for a deported display or for use with other SBCs and controllers (it’s compatible with a lot of them, like Arduino and ESP32), but not really for a quick test on a Raspberry Pi.

If you use a standard Raspberry Pi model, the HAT can be plugged directly into the GPIO pins. While your Pi is off, put the HAT on the GPIO pins and press it gently. Then, you can turn it on and follow the steps below for setting up the software.

The main benefit of using Raspberry Pi HATs is that they’re almost plug-and-play. You don’t have to deal with the annoying parts of wiring and soldering that you may have to do with other devices and accessories.

If you don’t use a Pi or need to use wires for your project, check the manual to see how to plug it in the right way. That shouldn’t be complicated either.

Raspberry Pi OS configuration

I highly recommend using Raspberry Pi OS for this, as everything will be smoother, as it’s built for it, and the HAT will be detected right away. I didn’t test on another system, but that’s usually how it goes.

I’m testing on a Lite version of Raspberry Pi, as no GUI is required for this kind of project.
If you need help setting up Raspberry Pi OS, check our tutorials. Once you have access to your session (via SSH, for example), here are the main steps to prepare your system for the display HAT:

  • Do the system updates, as usual:
    sudo apt update
    sudo apt upgrade -y

  • Enable SPI:
    sudo raspi-config
    Go to Interface Options > SPI and confirm to enable it.
  • Reboot the system:
    sudo reboot

Installing dependencies

After completing the basic setup, you’ll need to install the main libraries before writing your first line of code. The official documentation was a bit messy, so I’ll try to simplify the steps here.

I’ll provide the Python requirements, but if you want to code in C or other languages, you’ll need to follow the manufacturer’s documentation. The requirements might also differ slightly based on the hardware you have. Here’s what’s needed for the Waveshare product I tested.

Recommended next step
Master Raspberry Pi in 30 Days

If you want a clearer path than jumping from tutorial to tutorial, my book gives you a step-by-step roadmap to really understand your Raspberry Pi.

Check the book here

Start by installing all these Python libraries:
sudo apt install python3-pip python3-pil python3-numpy python3-setuptools python3-gpiozero python3-spidev

Tip: Command lines can be a pain to memorize. I put the essential Linux commands on a printable cheat sheet so you don't have to keep googling them. You can grab the PDF here if you want to save some time.

First Steps with an E-Ink Display

Now, let’s look at some code. We’ll start by running a script provided by the manufacturer without trying to understand it (to make sure everything works). Then, we’ll create a simple script and improve it step by step so you understand the logic behind most scripts using an e-ink display.

Run the demo

Waveshare (like most manufacturers) provides a demo to test the display. I highly recommend starting with this before writing your own script. This will ensure that everything is working with their code and that the setup is ready to use.

These are instructions for the Waveshare 2.13″ display:

  • Download the demo code with:
    wget https://files.waveshare.com/upload/7/71/E-Paper_code.zip
  • Extract the files:
    unzip E-Paper_code.zip -d ePaper
  • Browse to the Python subfolder for Raspberry Pi:
    cd ePaper/RaspberryPi_JetsonNano/python/examples
  • Try the demo script:
    python3 epd_2in13_V4_test.py
    Check the documentation to know the exact demo script to try with your hardware. The format seems to be epd_SIZE_VERSION_test.py for Waveshare.

If all goes well, your display will start to blink and show different layouts and content. Wait until you see text and shapes on the screen before moving forward. Fix any errors you might have at this step. You won’t be able to make it work with your own script if the demo doesn’t work.

Make the lib available in your own project files

If you don’t want to keep working inside the demo folder and create a new folder for your project or tests, you need to make the library available system-wide.

There are probably different ways to do this, but I copied the library folder to my current project. You can do the same using these commands:
cd <YOUR PROJECT>
cp /home/$USER/ePaper/RaspberryPi_JetsonNano/python/lib/waveshare_epd ./ -r

Adjust the command lines above to fit your project structure and the path where you downloaded the files, but that should be very close. Then test a minimalist script (like the Hello World below to make sure it’s working).

Note: I didn’t use pip or create a virtual environment, even though it’s an option. I did this to keep things as simple as possible. This could be a good idea for long-term projects.

Display your first text (Hello World)

At this stage, I recommend creating a minimal script to make sure the library is properly detected in your project folder, so you can use the display as expected.

I won’t explain the basic Python syntax in this tutorial. If you’re new to this, you might want to read our other tutorials first. Here is my beginner’s guide for using Python on Raspberry Pi, along with a cheat sheet of the main syntax elements. If you need a more step-by-step approach, you can also get my e-book, “Master Python on Raspberry Pi“.

Anyway, like most projects, I first wrote a basic “Hello World” script. Here is the full script you can copy and paste. I’ll explain the main parts just after:

import time
from PIL import Image, ImageDraw, ImageFont
from waveshare_epd import epd2in13_V4

# 1. Setup
epd = epd2in13_V4.EPD()
epd.init()
epd.Clear()

# 2. Create Image (using e-paper dimensions)
image = Image.new('1', (epd.width, epd.height), 255) #255 = white background
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()

# 3. Draw directly
draw.text((10, 10), 'Hello Pi', font=font, fill=0)

# 4. Push to screen
epd.display(epd.getbuffer(image))

# 5. Cleanup
time.sleep(2)
epd.sleep()

As you can see with the comments, there are a few mandatory steps you’ll basically use all the time when working with that kind of display:

  • Import the required libraries: In this case, we mostly need the Waveshare driver, but also an image management library like Pillow.
  • Initialization: Define the screen as explained in the manual, and clear it to start from scratch.
  • Create a blank canvas: You need to think of those scripts as image generators. You start with a blank image, add all the elements you want onto it, and only then interact with the screen to display the result.
  • Update the image: the “draw.text” function is the simplest thing you can use to edit this canvas to add some simple text on it.
  • Push to the screen: once the text is added, we use the “display” function to update the screen with our text.
  • Then we pause for a moment. The screen shouldn’t refresh too often. Let it sleep if you don’t update it after that. Remember that it doesn’t need energy to keep showing your text.

Take a few minutes to understand this logic, because we will use it in all the examples that follow. My goal now is to improve this and to show you other useful functions that can be used to do various things. But the main idea is laid out in this basic script.

Simple edits to try

The previous example uses a default font with a small size, which can be hard to read. If you want to display something bigger, you’ll need to have a font installed. I’ve tested with “Deja vu”, installed with:
sudo apt install fonts-dejavu

You can then use it like this instead of loading the default font:
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 24)
The last number is the font size. Adjust it based on the length of the text.

I also use the screen in landscape mode instead of portrait mode. This allows you to write longer texts or use a bigger font to make them more readable.

Weirdly, I don’t think there is a simple way to do this directly with the text() function. I had to create a temporary image with the text on it and then rotate it 90°. And you’ll likely want to center it or at least have some kind of dynamic positioning, which adds another layer of complexity.

Anyway, here is my script for all of these settings (font, font size, landscape, and center). You can start with this and adjust it based on your needs. And you find an easier way, let me know!

import time
from PIL import Image, ImageDraw, ImageFont
from waveshare_epd import epd2in13_V4

# Initialize the e-Paper display
epd = epd2in13_V4.EPD()
epd.init()
epd.Clear()

# Create a blank image for drawing
width, height = epd.width, epd.height
image = Image.new('1', (width, height), 255)
draw = ImageDraw.Draw(image)

# Load a font
try:
    font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 28)
except:
    font = ImageFont.load_default()

# Get the exact size of the text so the "sticker" isn't too big
text = 'RaspberryTips'
bbox = font.getbbox(text) # Returns (left, top, right, bottom)
text_width = bbox[2]
text_height = bbox[3]

# Create a temporary image for the text
text_img = Image.new('1', (text_width, text_height), 255) 
text_draw = ImageDraw.Draw(text_img)
text_draw.text((0, 0), text, font=font, fill=0)

# Rotate this text image to display it horizontally
rotated_text_img = text_img.rotate(90, expand=True)

# Center it on the screen
margin_left = (epd.width - text_height) // 2 #Top in landscape
margin_top = (epd.height - text_width) // 2 #Right in landscape
image.paste(rotated_text_img, (margin_left, margin_top))

# Display the image
epd.display(epd.getbuffer(image))
time.sleep(2)

# Put the display to sleep
epd.sleep()

And here is the result of this script:

I hope this makes sense. The tricky part is figuring out the margin calculations based on the font and font size, but you can skip that and use fixed numbers you get by trial and error if you display something that’s pretty static and predictable.

The rest of the steps are pretty much the same as in the previous version: imports, initialization, generating an image with text on it, sending it to the screen, and then sleep.


🛠 This tutorial doesn't work anymore? Report the issue here, so that I can update it!

Still stuck after following this guide? Drop your question in the RaspberryTips Community — real Pi users answer fast. Post your question here.

Going Further with the E-Ink Display

I don’t want to overwhelm you with complex concepts, but I’ve spent some time finding some useful functions you’ll probably need for your projects.

I’m sharing them in this last section so you don’t have to guess too much. Pick the one you’re interested in, and try to use it in your projects.

Partial refresh

You might’ve noticed that each time you change your text in our last example, the screen blinks for a few seconds. So, if you try to display the time clock with a Python loop, that won’t give you the best result.

To avoid this issue, you can ask the screen to do a partial refresh, using this function instead at the end of the script:
epd.displayPartial(epd.getbuffer(image))

So, if you want to try it by displaying the time on the screen, we can do it this way:

import time
from PIL import Image, ImageDraw, ImageFont
from waveshare_epd import epd2in13_V4

# Initialize the e-Paper display
epd = epd2in13_V4.EPD()
epd.init()
epd.Clear()

# Define font
try:
    font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 20)
except:
    font = ImageFont.load_default()

# Create image
image = Image.new('1', (epd.width, epd.height), 255)
draw = ImageDraw.Draw(image)

try:
    while True:
        text = time.strftime("%H:%M:%S")

        # Clear the canvas
        draw.rectangle((0, 0, epd.width, epd.height), fill=255)

        # Draw text directly on the main image
        draw.text((5, 10), text, font=font, fill=0)

        # Display the image
        epd.displayPartial(epd.getbuffer(image))
        
        # Refresh every second or so
        time.sleep(1)

except KeyboardInterrupt:
    print("Stopping...")
    epd.init() 
    epd.Clear()
    epd.sleep()

Basically, the core of it stays the same as in our “Hello World” example, except we use “displayPartial” instead of “display”.

I’ve added a loop to send the display update at regular intervals and get the current time from the strftime() function. I’ve also added a try/except structure, so you can see how to handle a clean stop when you press CTRL+C to stop the Python script (it will clear the screen).

Create a live dashboard

Once you know how to put text on the screen and refresh it regularly, the next step is probably something like a live dashboard showing key data points, so you can see how your projects (or system) are doing.

As an example, I did an experiment showing the current system load. I’ll show you how to get this information in Python and, most importantly, how to display text on multiple lines.

Once again, here’s the full script. I’ll explain later:

import time
import os
import psutil #sudo apt install python3-psutil
from PIL import Image, ImageDraw, ImageFont
from waveshare_epd import epd2in13_V4

# Initialize
epd = epd2in13_V4.EPD()
epd.init()
epd.Clear()
time.sleep(1)

# Setup base image (Portrait for the hardware)
image = Image.new('1', (epd.width, epd.height), 255)
epd.displayPartial(epd.getbuffer(image)) # Show blank screen immediately

try:
    font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 16)
except:
    font = ImageFont.load_default()

try:
    while True:
        # 1. Fetch Stats
        load_1, load_5, load_15 = os.getloadavg()
        ram = psutil.virtual_memory()
        
        # Format strings
        line1 = f"Load: {float(load_1):.2f}, {float(load_5):.2f}"
        line2 = f"RAM: {ram.percent}% used"
        line3 = time.strftime("%H:%M:%S")
        
        full_text = f"{line1}\n{line2}\n{line3}"

        # 2. Prepare Drawing
        draw = ImageDraw.Draw(image)
        draw.rectangle((0, 0, epd.width, epd.height), fill=255) # Clear frame

        # Use multiline_textbbox for multiple lines
        bbox = draw.multiline_textbbox((0, 0), full_text, font=font)
        tw, th = bbox[2], bbox[3]

        # 3. Create Sticker
        text_img = Image.new('1', (tw, th), 255)
        text_draw = ImageDraw.Draw(text_img)
        text_draw.multiline_text((0, 0), full_text, font=font, fill=0, align="center")
        
        # 4. Rotate and Paste
        rotated_text = text_img.rotate(90, expand=True)
        
        # Calculate margins for centering
        margin_left = (epd.width - th) // 2
        margin_top = (epd.height - tw) // 2
        image.paste(rotated_text, (margin_left, margin_top))

        # 5. Partial Update
        epd.displayPartial(epd.getbuffer(image))
        
        time.sleep(5) # Update every 5 seconds is plenty for a monitor

except KeyboardInterrupt:
    print("Stopping...")
    epd.init()
    epd.Clear()
    epd.sleep()

We’re building on the previous scripts, so you should already understand most of it (the main parts are the same, including the rotate thing, margin calculations, etc.).

Here are the main changes:

  • I use the “psutil” and “os” libraries to get the system load (load average and RAM usage). Values are stored in specific variables (load_1, ram, etc.).
  • I use 3 text variables, one for each text line, with a legend and the value collected in the last step. If you’re not familiar with the syntax I use to integrate these, you might want to check out Master Python on Raspberry Pi or some documentation about it.
  • Then, I merge the three lines into the full_text variable (separated with line breaks, using “\n”).
  • We use functions like multiline_text instead of text. They work pretty much the same, but for multiple lines of text (hence the name!).

The end of the script is the same as usual. If it’s working as expected, you should see something like that:

Note: If you get an error with the psutil library, you probably need to install it, as it’s not preinstalled on Raspberry Pi OS Lite:
sudo apt install python3-psutil

Display an image

Interestingly, the easiest thing to do with that kind of accessory is to display an image on the screen.

Up to this point, we’ve been creating an image with the elements we wanted on it, and then sending it to the screen. Well, if you already have an image in a compatible format (like PNG), you can send it right away.

Here’s a simple example:

from waveshare_epd import epd2in13_V4
from PIL import Image

# 1. Initialize
epd = epd2in13_V4.EPD()
epd.init()
epd.Clear()

# 2. Open the image
# It's best if the PNG is already 250x122 (landscape) or 122x250 (portrait)
img = Image.open('epaper.png')

# 3. Convert to 1-bit color (Black and White)
# '1' mode is required for the e-paper buffer
img = img.convert('1')

# 4. Handle Rotation (if needed)
img_rotated = img.rotate(90, expand=True)

# 5. Display
epd.display(epd.getbuffer(img_rotated))

# 6. Sleep
epd.sleep()

Pretty simple, right?

The trick is to use an image that’s the right size so you don’t have to handle the resizing and positioning in your script.

By the way, if you already know how to generate “dynamic” images in Python using other libraries than the ones I used previously, it might be easier for you to create a temporary PNG file and then send it to the screen using this all the time.

Display multiple elements on the screen

So, the last step after that (or at least where I stopped) is to mix a bunch of different elements on the same screen. If you make the badge I mentioned, you’ll have the company logo (image), the name (text), and maybe even some dynamic elements.

To do something like that, you’ll sequentially add the elements to your main canvas, one at a time, something like:

import time
from PIL import Image, ImageDraw, ImageFont
from waveshare_epd import epd2in13_V4

epd = epd2in13_V4.EPD()
epd.init()

# 1. Create the main canvas
canvas = Image.new('1', (epd.height, epd.width), 255) 
draw = ImageDraw.Draw(canvas)

# 2. Add the image (Logo/Icon)
icon = Image.open('epaper.png').convert('1')
canvas.paste(icon, (-10, -20)) 

# 3. Add the text
try:
    font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 20)
except:
    font = ImageFont.load_default()

text = "Patrick FROMAGET"
bbox = font.getbbox(text)
tw, th = bbox[2], bbox[3]

# Adjust margins and positions
text_x = (epd.height - tw) // 2
text_y = 90
draw.text((text_x, text_y), text, font=font, fill=0)

# 4. Final rotation & display
final_image = canvas.rotate(90, expand=True)
epd.displayPartial(epd.getbuffer(final_image))

epd.sleep()

There’s nothing new in this script. We just reused the same functions, but added elements to the canvas two times (and you can add as many as you need). I went with “displayPartial” in case you add dynamic elements, but if it’s a simple badge, use “display”.

If you’ve gone through all the examples I gave you, you should feel way more confident now. Please share what you’ve done and let me know if you’ve found any optimizations for my examples. As I mentioned in the intro, I’m definitely not a pro with this.

Just so you know, you can find the Waveshare documentation here, and there are some examples on their GitHub. I was a bit disappointed to find that it wasn’t as well-documented as I’d expect for something as popular, so I hope this article helped you to get started.

Not getting the same result?

Even when you follow every step, small differences in OS version, hardware or config can change the outcome. Instead of wasting time guessing, get help from people who have already fixed the same kind of issue.

Ask your question now →
🔒 No risk. Cancel anytime.
  • Get help on your exact issue
  • Access step-by-step videos for tricky setups
  • Browse the website without ads

Similar Posts

Leave a Reply

Community members only
Comments are reserved for RaspberryTips Community members. Join the community to ask questions, get help, and interact with other Raspberry Pi users.

Join the community  or  log in