Skip to content

Commit

Permalink
Merge pull request #31 from fimad/IT8951
Browse files Browse the repository at this point in the history
Add support for IT8951 based displays
  • Loading branch information
joukos authored Nov 3, 2019
2 parents e314431 + 0339d26 commit 7e405bf
Show file tree
Hide file tree
Showing 2 changed files with 312 additions and 2 deletions.
309 changes: 309 additions & 0 deletions drivers/driver_it8951.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
from PIL import Image
from drivers.drivers_base import DisplayDriver
import array
import struct
import time
try:
import spidev
import RPi.GPIO as GPIO
except ImportError:
pass

class IT8951(DisplayDriver):
"""A generic driver for displays that use a IT8951 controller board.
This class will automatically infer the width and height by querying the
controller."""

RST_PIN = 17
CS_PIN = 8
BUSY_PIN = 24

VCOM = 2000

CMD_GET_DEVICE_INFO = [0x03, 0x02]
CMD_WRITE_REGISTER = [0x00, 0x11]
CMD_READ_REGISTER = [0x00, 0x10]
CMD_DISPLAY_AREA = [0x00, 0x34]
CMD_VCOM = [0x00, 0x39]
CMD_LOAD_IMAGE_AREA = [0x00, 0x21]
CMD_LOAD_IMAGE_END = [0x00, 0x22]

REG_SYSTEM_BASE = 0
REG_I80CPCR = REG_SYSTEM_BASE + 0x04

REG_DISPLAY_BASE = 0x1000
REG_LUTAFSR = REG_DISPLAY_BASE + 0x224 # LUT Status Reg (status of All LUT Engines)

REG_MEMORY_CONV_BASE_ADDR = 0x0200
REG_MEMORY_CONV = REG_MEMORY_CONV_BASE_ADDR + 0x0000
REG_MEMORY_CONV_LISAR = REG_MEMORY_CONV_BASE_ADDR + 0x0008

ROTATE_0 = 0
ROTATE_90 = 1
ROTATE_180 = 2
ROTATE_270 = 3

BPP_2 = 0
BPP_3 = 1
BPP_4 = 2
BPP_8 = 3

LOAD_IMAGE_L_ENDIAN = 0
LOAD_IMAGE_B_ENDIAN = 1

# Erases the display and leaves it in a white state regardless of what image
# data is currently in memory.
DISPLAY_UPDATE_MODE_INIT = 0
# A fast non-flashy update mode that can go from any gray scale color to
# black or white.
DISPLAY_UPDATE_MODE_DU = 1
# A flashy update mode that can go from any gray scale color to any other
# gray scale color.
DISPLAY_UPDATE_MODE_GC16 = 2
# For more documentation on display update modes see the reference document:
# http://www.waveshare.net/w/upload/c/c4/E-paper-mode-declaration.pdf

def __init__(self):
super().__init__()
self.name = "IT8951"
self.supports_partial = True

def delay_ms(self, delaytime):
time.sleep(float(delaytime) / 1000.0)

def spi_write(self, data):
"""Write raw bytes over SPI."""
self.SPI.writebytes(data)

def spi_read(self, n):
"""Read n raw bytes over SPI."""
return self.SPI.readbytes(n)

def write_command(self, command):
self.wait_for_ready()
GPIO.output(self.CS_PIN, GPIO.LOW)
self.spi_write([0x60, 0x00])
self.wait_for_ready()
self.spi_write(command)
GPIO.output(self.CS_PIN, GPIO.HIGH)

def write_data_bytes(self, data):
max_transfer_size = 4096
self.wait_for_ready()
GPIO.output(self.CS_PIN, GPIO.LOW)
self.spi_write([0x00, 0x00])
self.wait_for_ready()
for i in range(0, len(data), max_transfer_size):
self.spi_write(data[i: i + max_transfer_size])
GPIO.output(self.CS_PIN, GPIO.HIGH)

def read_bytes(self, n):
self.wait_for_ready()
GPIO.output(self.CS_PIN, GPIO.LOW)
self.spi_write([0x10, 0x00])
self.wait_for_ready()
self.spi_write([0x00, 0x00]) # Two bytes of dummy data.
self.wait_for_ready()
result = array.array("B", self.spi_read(n))
GPIO.output(self.CS_PIN, GPIO.HIGH)
return result

def write_data_half_word(self, half_word):
"""Writes a half word of data to the controller.
The standard integer format for passing data to and from the controller
is little endian 16 bit words.
"""
self.write_data_bytes([(half_word >> 8) & 0xFF, half_word & 0xFF])

def read_half_word(self):
"""Reads a half word of from the controller."""
return struct.unpack(">H", self.read_bytes(2))[0]

def write_register(self, register_address, value):
self.write_command(self.CMD_WRITE_REGISTER)
self.write_data_half_word(register_address)
self.write_data_half_word(value)

def read_register(self, register_address):
self.write_command(self.CMD_READ_REGISTER)
self.write_data_half_word(register_address)
return self.read_half_word()

def wait_for_ready(self):
"""Waits for the busy pin to drop.
When the busy pin is high the controller is busy and may drop any
commands that are sent to it."""
while GPIO.input(self.BUSY_PIN) == 0:
self.delay_ms(100)

def wait_for_display_ready(self):
"""Waits for the display to be finished updating.
It is possible for the controller to be ready for more commands but the
display to still be refreshing. This will wait for the display to be
stable."""
while self.read_register(self.REG_LUTAFSR) != 0:
self.delay_ms(100)

def get_vcom(self):
self.wait_for_ready()
self.write_command(self.CMD_VCOM)
self.write_data_half_word(0)
return self.read_half_word()

def set_vcom(self, vcom):
self.write_command(self.CMD_VCOM)
self.write_data_half_word(1)
self.write_data_half_word(vcom)

def fixup_string(self, s):
result = ""
for i in range(0, len(s), 2):
result += "%c%c" % (s[i + 1], s[i])
null_index = result.find("\0")
if null_index != -1:
result = result[0:null_index]
return result

def init(self, **kwargs):
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(self.RST_PIN, GPIO.OUT)
GPIO.setup(self.CS_PIN, GPIO.OUT)
GPIO.setup(self.BUSY_PIN, GPIO.IN)
self.SPI = spidev.SpiDev(0, 0)
self.SPI.max_speed_hz = 2000000
self.SPI.mode = 0b00

# It is unclear why this is necessary but it appears to be. The sample
# code from WaveShare [1] manually controls the CS bin and has its state
# span multiple SPI operations.
#
# [1] https://github.com/waveshare/IT8951
self.SPI.no_cs = True

GPIO.output(self.CS_PIN, GPIO.HIGH)

# Reset the device to its initial state.
GPIO.output(self.RST_PIN, GPIO.LOW)
self.delay_ms(500)
GPIO.output(self.RST_PIN, GPIO.HIGH)
self.delay_ms(500)

self.write_command(self.CMD_GET_DEVICE_INFO);

(
self.width,
self.height,
img_addr_l,
img_addr_h,
firmware_version,
lut_version,
) = struct.unpack(">HHHH16s16s", self.read_bytes(40))
firmware_version = self.fixup_string(firmware_version)
lut_version = self.fixup_string(lut_version)
self.img_addr = img_addr_h << 16 | img_addr_l

print("width = %d" % self.width)
print("height = %d" % self.height)
print("img_addr = %08x" % self.img_addr)
print("firmware = %s" % firmware_version)
print("lut = %s" % lut_version)

# Ensure that the returned device info looks sane. If it doesn't, then
# there is little chance that any of the other operations are going to
# do anything.
assert self.img_addr != 0
assert self.width != 0
assert self.height != 0

# Set to Enable I80 Packed mode.
self.write_register(self.REG_I80CPCR, 0x0001)

if self.VCOM != self.get_vcom():
self.set_vcom(self.VCOM)
print("VCOM = -%.02fV" % (self.get_vcom() / 1000.0))

# Initialize the display with a blank image.
self.wait_for_ready()
image = Image.new("L", (self.width, self.height), 0x255)
self.draw(0, 0, image, self.DISPLAY_UPDATE_MODE_INIT)

def display_area(self, x, y, w, h, display_mode):
self.write_command(self.CMD_DISPLAY_AREA)
self.write_data_half_word(x)
self.write_data_half_word(y)
self.write_data_half_word(w)
self.write_data_half_word(h)
self.write_data_half_word(display_mode)

def draw(self, x, y, image, update_mode_override=None):
width = image.size[0]
height = image.size[1]

self.wait_for_display_ready()

self.write_register(
self.REG_MEMORY_CONV_LISAR + 2, (self.img_addr >> 16) & 0xFFFF)
self.write_register(self.REG_MEMORY_CONV_LISAR, self.img_addr & 0xFFFF)

# Define the region being loaded.
self.write_command(self.CMD_LOAD_IMAGE_AREA)
self.write_data_half_word(
(self.LOAD_IMAGE_L_ENDIAN << 8) |
(self.BPP_4 << 4) |
self.ROTATE_0)
self.write_data_half_word(x)
self.write_data_half_word(y)
self.write_data_half_word(width)
self.write_data_half_word(height)

self.write_data_bytes(self.pack_image(image))
self.write_command(self.CMD_LOAD_IMAGE_END);

if update_mode_override is not None:
update_mode = update_mode_override
elif image.mode == "1":
# Use a faster, non-flashy update mode for pure black and white
# images.
update_mode = self.DISPLAY_UPDATE_MODE_DU
else:
# Use a slower, flashy update mode for gray scale images.
update_mode = self.DISPLAY_UPDATE_MODE_GC16
# Blit the image to the display
self.display_area(x, y, width, height, update_mode)

def pack_image(self, image):
"""Packs a PIL image for transfer over SPI to the driver board."""
# Convert the image to 8 bit / BW. Then converting to a smaller
# bits-per-pixel gray scale image is just a matter of chopping off the
# least significant bytes.
image_grey = image.convert("L")
pixels = image_grey.load()
frame_buffer = [
pixels[x, y]
for y in range(image.height)
for x in range(image.width)
]

# For now, only 4 bit packing is supported. Theoretically we could
# achieve a transfer speed up by using 2 bit packing for black and white
# images. However, 2bpp doesn't seem to play well with the DU rendering
# mode.
packed_buffer = []
for i in range(0, len(frame_buffer), 2):
value = (frame_buffer[i] >> 4) & 0x0F
if i + 1 < len(frame_buffer):
value |= frame_buffer[i + 1] & 0xF0
packed_buffer += [value]

# The driver board assumes all data is read in as 16bit ints. To match
# the endianness every pair of bytes must be swapped.
for i in range(0, len(packed_buffer), 2):
packed_buffer[i], packed_buffer[i + 1] = (
packed_buffer[i + 1], packed_buffer[i])

return packed_buffer
5 changes: 3 additions & 2 deletions papertty.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import drivers.drivers_full as drivers_full
import drivers.drivers_color as drivers_color
import drivers.drivers_colordraw as drivers_colordraw
import drivers.driver_it8951 as driver_it8951

# for ioctl
import fcntl
Expand Down Expand Up @@ -104,7 +105,7 @@ def font_height(font, spacing=0):
def band(bb):
"""Stretch a bounding box's X coordinates to be divisible by 8,
otherwise weird artifacts occur as some bits are skipped."""
return (bb[0] & 0xF8, bb[1], (bb[2] + 8) & 0xF8, bb[3]) if bb else None
return (int(bb[0] / 8) * 8, bb[1], int((bb[2] + 7) / 8) * 8, bb[3]) if bb else None

@staticmethod
def split(s, n):
Expand Down Expand Up @@ -315,7 +316,7 @@ def get_drivers():
drivers_full.EPD2in7, drivers_full.EPD4in2, drivers_full.EPD7in5,
drivers_color.EPD4in2b, drivers_color.EPD7in5b, drivers_color.EPD5in83, drivers_color.EPD5in83b,
drivers_colordraw.EPD1in54b, drivers_colordraw.EPD1in54c, drivers_colordraw.EPD2in13b,
drivers_colordraw.EPD2in7b, drivers_colordraw.EPD2in9b,
drivers_colordraw.EPD2in7b, drivers_colordraw.EPD2in9b, driver_it8951.IT8951,
drivers_base.Dummy, drivers_base.Bitmap]
for driver in driverlist:
driverdict[driver.__name__] = {'desc': driver.__doc__, 'class': driver}
Expand Down

0 comments on commit 7e405bf

Please sign in to comment.