Raspberry Pi Pico
Pimoroni inky:bit

The inky:bit is a micro:bit accessory which is strictly for version 2.0 of the micro:bit. It is a gorgeous epaper display. I have had one of these since its release and haven't used it a great deal with the micro:bit. I thought it would be worth having a go at connecting it to the Pico.

In the photograph, I have connected the inky:bit using a Pinbetween. There are numerous connections to make.

  • micro:bit pin2 to Pico GP2
  • micro:bit pin8 to Pico GP3
  • micro:bit pin12 to Pico GP4
  • micro:bit pin16 to Pico GP5
  • micro:bit pin13 to Pico GP14
  • micro:bit pin14 to Pico GP15

Pico Circuit

I needed a font to use for the project, It is the one I have used in several projects and is saved as font.py.

font7x5 = bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x5f\x00\x00\x00\x07\x00\x07\x00\x14\x7f\x14'
                b'\x7f\x14\x24\x2a\x7f\x2a\x12\x23\x13\x08\x64\x62\x36\x49\x55\x22\x50\x00'
                b'\x05\x03\x00\x00\x00\x1c\x22\x41\x00\x00\x41\x22\x1c\x00\x14\x08\x3e\x08'
                b'\x14\x08\x08\x3e\x08\x08\x00\x50\x30\x00\x00\x08\x08\x08\x08\x08\x00\x60'
                b'\x60\x00\x00\x20\x10\x08\x04\x02\x3e\x51\x49\x45\x3e\x00\x42\x7f\x40\x00'
                b'\x42\x61\x51\x49\x46\x21\x41\x45\x4b\x31\x18\x14\x12\x7f\x10\x27\x45\x45'
                b'\x45\x39\x3c\x4a\x49\x49\x30\x01\x71\x09\x05\x03\x36\x49\x49\x49\x36\x06'
                b'\x49\x49\x29\x1e\x00\x36\x36\x00\x00\x00\x56\x36\x00\x00\x08\x14\x22\x41'
                b'\x00\x14\x14\x14\x14\x14\x00\x41\x22\x14\x08\x02\x01\x51\x09\x06\x32\x49'
                b'\x79\x41\x3e\x7e\x11\x11\x11\x7e\x7f\x49\x49\x49\x36\x3e\x41\x41\x41\x22'
                b'\x7f\x41\x41\x22\x1c\x7f\x49\x49\x49\x41\x7f\x09\x09\x09\x01\x3e\x41\x49'
                b'\x49\x7a\x7f\x08\x08\x08\x7f\x00\x41\x7f\x41\x00\x20\x40\x41\x3f\x01\x7f'
                b'\x08\x14\x22\x41\x7f\x40\x40\x40\x40\x7f\x02\x0c\x02\x7f\x7f\x04\x08\x10'
                b'\x7f\x3e\x41\x41\x41\x3e\x7f\x09\x09\x09\x06\x3e\x41\x51\x21\x5e\x7f\x09'
                b'\x19\x29\x46\x46\x49\x49\x49\x31\x01\x01\x7f\x01\x01\x3f\x40\x40\x40\x3f'
                b'\x1f\x20\x40\x20\x1f\x3f\x40\x38\x40\x3f\x63\x14\x08\x14\x63\x07\x08\x70'
                b'\x08\x07\x61\x51\x49\x45\x43\x00\x7f\x41\x41\x00\x02\x04\x08\x10\x20\x00'
                b'\x41\x41\x7f\x00\x04\x02\x01\x02\x04\x40\x40\x40\x40\x40\x00\x01\x02\x04'
                b'\x00\x20\x54\x54\x54\x78\x7f\x48\x44\x44\x38\x38\x44\x44\x44\x20\x38\x44'
                b'\x44\x48\x7f\x38\x54\x54\x54\x18\x08\x7e\x09\x01\x02\x0c\x52\x52\x52\x3e'
                b'\x7f\x08\x04\x04\x78\x00\x44\x7d\x40\x00\x20\x40\x44\x3d\x00\x7f\x10\x28'
                b'\x44\x00\x00\x41\x7f\x40\x00\x7c\x04\x18\x04\x78\x7c\x08\x04\x04\x78\x38'
                b'\x44\x44\x44\x38\x7c\x14\x14\x14\x08\x08\x14\x14\x18\x7c\x7c\x08\x04\x04'
                b'\x08\x48\x54\x54\x54\x20\x04\x3f\x44\x40\x20\x3c\x40\x40\x20\x7c\x1c\x20'
                b'\x40\x20\x1c\x3c\x40\x30\x40\x3c\x44\x28\x10\x28\x44\x0c\x50\x50\x50\x3c'
                b'\x44\x64\x54\x4c\x44\x00\x08\x36\x41\x00\x00\x00\x7f\x00\x00\x00\x41\x36'
                b'\x08\x00\x10\x08\x08\x10\x08\x00\x00\x00\x00\x00')

This is a copy and update of the Pimoroni library. I have added a couple of features and rewritten the text library. It is saved as inky.py.

from micropython import const
from time import sleep
from font import font7x5
from math import sqrt
# 250 x 122 effective pixels on the display
COLS = const(136)
ROWS = const(250)
OFFSET_X = const(0)
OFFSET_Y = const(6)

WIDTH = const(250)
HEIGHT = const(122)

DRIVER_CONTROL = const(0x01)
GATE_VOLTAGE = const(0x03)
SOURCE_VOLTAGE = const(0x04)
DISPLAY_CONTROL = const(0x07)
NON_OVERLAP = const(0x0B)
BOOSTER_SOFT_START = const(0x0C)
GATE_SCAN_START = const(0x0F)
DEEP_SLEEP = const(0x10)
DATA_MODE = const(0x11)
SW_RESET = const(0x12)
TEMP_WRITE = const(0x1A)
TEMP_READ = const(0x1B)
TEMP_CONTROL = const(0x1C)
TEMP_LOAD = const(0x1D)
MASTER_ACTIVATE = const(0x20)
DISP_CTRL1 = const(0x21)
DISP_CTRL2 = const(0x22)
WRITE_RAM = const(0x24)
WRITE_ALTRAM = const(0x26)
READ_RAM = const(0x25)
VCOM_SENSE = const(0x28)
VCOM_DURATION = const(0x29)
WRITE_VCOM = const(0x2C)
READ_OTP = const(0x2D)
WRITE_LUT = const(0x32)
WRITE_DUMMY = const(0x3A)
WRITE_GATELINE = const(0x3B)
WRITE_BORDER = const(0x3C)
SET_RAMXPOS = const(0x44)
SET_RAMYPOS = const(0x45)
SET_RAMXCOUNT = const(0x4E)
SET_RAMYCOUNT = const(0x4F)
NOP = const(0xFF)

CS_ACTIVE = const(0)
CS_INACTIVE = const(0)

TEXT_TINY = const(1)
TEXT_NORMAL = const(2)
TEXT_MEDIUM = const(3)
TEXT_LARGE = const(4)

WHITE = const(0)
BLACK = const(1)
ACCENT = const(2)

LUTS_BLACK = bytearray([
    0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69,
    0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00,
    0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00
])

class InkyBit:
    
    def __init__(self, spi, dc, cs, reset, busy):
        self.spi = spi
        self.dc = dc
        self.cs = cs
        self.reset = reset
        self.busy = busy
        self.buf_b = bytearray(b'\xFF' * (COLS // 8) * ROWS)
        self.buf_r = bytearray((COLS // 8) * ROWS)
    
    def clear(self):
        self.buf_b = bytearray(b'\xFF' * (COLS // 8) * ROWS)
        self.buf_r = bytearray((COLS // 8) * ROWS)
    
    def set_pixel(self, x, y, color = 1):
        y += OFFSET_Y
        y = COLS - 1 - y
        shift = 7 - y % 8
        y //= 8
         
        offset = x * (COLS // 8) + y
         
        if offset >= len(self.buf_b):
            return
        
        byte_b = self.buf_b[offset] | (0b1 << shift)
        byte_r = self.buf_r[offset] & ~(0b1 << shift)

        if color == 2:
            # Set a bit to set as red/yellow
            byte_r |= 0b1 << shift
        if color == 1:
            # Mask *out* a bit to set as black
            byte_b &= ~(0b1 << shift)

        self.buf_b[offset] = byte_b
        self.buf_r[offset] = byte_r
    
    def draw_line(self, x0, y0, x1, y1, color=1):
        dx = abs(x1 - x0)
        sx = 1 if x0 < x1 else -1
        dy = -abs(y1 - y0)
        sy = 1 if y0 < y1 else -1
        
        err = dx + dy
        while True:
            self.set_pixel(x0, y0, color)
            if x0==x1 and y0==y1:
                break
            e2 = 2 * err
            if e2 > dy:
                err += dy
                x0 += sx
            if e2 <= dx:
                err += dx
                y0 += sy
    
    def draw_rectangle(self, x, y, width, height, color=1, filled=False):
        width -= 1
        height -= 1
        self.draw_line(x, y, x + width, y, color)
        self.draw_line(x, y, x, y + height, color)
        self.draw_line(x + width, y, x + width, y + height, color)
        self.draw_line(x, y + height, x + width, y + height, color)

        if filled:
            x += 1
            y += 1
            width -= 1
            height -= 1
            for px in range(width):
                for py in range(height):
                    self.set_pixel(x + px, y + py, color)        
    
    def draw_circle(self, cx, cy, r, color = 1, filled = False):
        d = (5 - r * 4) // 4
        x = 0
        y = r
        while x <= y:
            self.set_pixel(cx + x, cy + y, color)
            self.set_pixel(cx + x, cy - y, color)
            self.set_pixel(cx - x, cy + y, color)
            self.set_pixel(cx - x, cy - y, color)
            self.set_pixel(cx + y, cy + x, color)
            self.set_pixel(cx + y, cy - x, color)
            self.set_pixel(cx - y, cy + x, color)
            self.set_pixel(cx - y, cy - x, color)
            if filled:
                self.draw_line(cx - x, cy + y, cx + x, cy + y, color)
                self.draw_line(cx - x, cy - y, cx + x, cy - y, color)
                self.draw_line(cx - y, cy + x, cx + y, cy + x, color)
                self.draw_line(cx - y, cy - x, cx + y, cy - x, color)
                
            if d < 0:
                d += 2* x + 1
            else:
                d += 2 * (x - y) + 1
                y -= 1
            x += 1
    
    def _spi_cmd(self, command, data = None):
        self.cs.value(CS_ACTIVE)
        self.dc.value(0)
        self.spi.write(bytearray([command]))
        if data is not None:
            self.dc.value(1)
            self.spi.write(bytearray(data))
        self.cs.value(CS_INACTIVE)
    
    def _spi_data(self, data):
        self.cs.value(CS_ACTIVE)
        self.dc.value(1)
        self.spi.write(bytearray(data))
        self.cs.value(CS_INACTIVE)
    
    def _busy_wait(self):
        while self.busy.value():
            v = busy.value()
            print(v)
            sleep(0.5)
       
    def draw_text(self, x, y, text, color = 1, scale = 1):
        if scale < 1 or scale>3: return
        starty = y
        for c in text:
            data = font7x5[(ord(c)-32)*5:(ord(c)-32)*5+5]
            for d in range(5):
                for k in range(scale):
                    y = starty
                    for i in range(7):                                        
                        for j in range(scale):
                            if data[d] >> i & 1:
                                self.set_pixel(x, y, color)
                            y += 1
                    x += 1                          
                        
    def show(self):
        self.spi.init()
        self.reset.value(0)
        sleep(0.5)
        self.reset.value(1)
        sleep(0.5)
        
        self._spi_cmd(0x12)
        sleep(1)
        self._busy_wait()
        self._spi_cmd(DRIVER_CONTROL, [ROWS - 1, (ROWS - 1) >> 8, 0x00])
        self._spi_cmd(WRITE_DUMMY, [0x1B])
        self._spi_cmd(WRITE_GATELINE, [0x0B])
        self._spi_cmd(DATA_MODE, [0x03])
        self._spi_cmd(SET_RAMXPOS, [0x00, COLS // 8 - 1])
        self._spi_cmd(SET_RAMYPOS, [0x00, 0x00, (ROWS - 1) & 0xFF, (ROWS - 1) >> 8])
        self._spi_cmd(WRITE_VCOM, [0x70])
        self._spi_cmd(WRITE_LUT, LUTS_BLACK)
        self._spi_cmd(SET_RAMXCOUNT, [0x00])
        self._spi_cmd(SET_RAMYCOUNT, [0x00, 0x00])

        self._spi_cmd(WRITE_RAM)
        self._spi_data(self.buf_b)

        self._spi_cmd(WRITE_ALTRAM)
        self._spi_data(self.buf_r)

        self._busy_wait()
        self._spi_cmd(MASTER_ACTIVATE)        

Here is my test code,

from machine import Pin, SPI
from time import sleep
from inky import InkyBit

spi = SPI(1, 10_000_000, sck=Pin(14), mosi=Pin(15))

dc = Pin(4, Pin.OUT)
cs = Pin(3, Pin.OUT)
reset = Pin(2, Pin.OUT)
busy = Pin(5, Pin.IN)

ink = InkyBit(spi, dc, cs, reset, busy)



# make a border with rectangles
ink.draw_rectangle(0 , 0, 250, 136, color = 2, filled = True)
ink.draw_rectangle(4 , 4, 242, 114, color = 0, filled = True)
# add a large title - inky:bit is 8 chars - 10px * 8 wide, 14px tall
ink.draw_text(81, 10, "inky:bit", scale=2)
# underline the title
ink.draw_line(81, 26, 161, 26, color=2)
ink.draw_line(81, 27, 161, 27, color=2)
# some diagonal lines
ink.draw_line(0, 0, 20, 20, color=2)
ink.draw_line(230, 20, 250, 0, color=2)
# do a couple of circles - 
ink.draw_circle(230, 20, 5, color = 2, filled = True)
ink.draw_circle(175, 16, 8)
# some more text, standard size
ink.draw_text(24, 40, "inky:bit is a micro:bit accessory.")
ink.draw_text(24, 60, "It can be controlled with a Pico.")
ink.draw_text(24, 80, "It's a nice-looking display.")
ink.draw_text(24, 100, "I should spend more time on the fonts.")
# some bullets to go with the text
ink.draw_rectangle(10, 42, 4, 4, filled=True)
ink.draw_rectangle(10, 62, 4, 4, filled=True)
ink.draw_rectangle(10, 82, 4, 4, filled=True)
ink.draw_rectangle(10, 102, 4, 4, filled=True)

ink.show()
print("Done")

The circle drawing, particularly the filled version needs some work to be quick enough to use. I could also put more work into the fonts. The display is working though and, in real life, looks really sharp.