quarta-feira, fevereiro 15, 2023

Examinando um Display e-paper - Parte 3

Já que não rolou usar o display com o CircuitPython (por enquanto, espero), resolvi tentar com o MicroPython. Obtive melhores resultados, mas tem algumas limitações a serem superadas.

E temos uma imagem no display!


MicroPython é o projeto original que implementou uma grande parte do Python 3 em microcontroladores. CircuitPython é um fork do MicroPython, capitaneado principalmente pela Adfruit. Ele mantem a parte da linguagem Python mas tem mudanças nas classes de interface com os recursos do microcontrolador.

No lugar do displayio do CircuitPython, a base para interface com displays gráficos no MicroPython é o framebuf. Como o nome sugere, esta classe foca na manipulação de um buffer onde é mantida a imagem da tela. Um driver para um display é uma classe derivada de framebuf que (no mínimo):

  • aloca o buffer de tela com o tamanho e formato apropriado na iniciação e o passa para a iniciação da classe framebuf (junto com as dimensões e o formato de armazenamento dos pontos)
  • tem um método show() que envia para o display o conteúdo do buffer de tela
A documentação de framebuf está em https://docs.micropython.org/en/latest/library/framebuf.html.Ela tem métodos para desenhar retas, elipses (na versão mais nova), bitmaps e texto no buffer.

Foi relativamente simples criar um driver a partir dos códigos que eu já tinha e funcionou rapidamente. O código deste primeiro driver está abaixo (perdão pelos comentários em inglês!).
# MH-ET-LIVE 1.54 e-paper display driver
# Based on Waveshare epd1in54_V2.py by Waveshare Team

from micropython import const
from time import sleep
import framebuf

# Screen size (fixed by now)
WIDTH = const(200)
HEIGHT = const(200)

# Commands
DRIVER_OUTPUT_CONTROL = const(0x01)
SET_GATE_VOLTAGE = const(0x03)
SET_SOURCE_VOLTAGE = const(0x04)
DEEP_SLEEP_MODE = const(0x10)
DATA_ENTRY_MODE = const(0x11)
SWRESET = const(0x12)
UNDOC_1 = const(0x18)
MASTER_ACTIVATION = const(0x20)
DISPLAY_UPDATE_CONTROL_2 = const(0x22)
WRITE_RAM_BLACK = const(0x24)
VCOM_VOLTAGE = const(0x2C)
WRITE_LUT_REGISTER = const(0x32)
BORDER_WAVEFORM = const(0x3C)
DRIVE_OUTPUT_CONTROL = const(0x3F)
SET_RAM_X_ADDRESS = const(0x44)
SET_RAM_Y_ADDRESS = const(0x45)
SET_RAM_X_CURSOR = const(0x4E)
SET_RAM_Y_CURSOR = const(0x4F)

# LUT
_LUT = (
    b"\x80\x48\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    b"\x40\x48\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    b"\x80\x48\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    b"\x40\x48\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    b"\x0a\x00\x00\x00\x00\x00\x00"
    b"\x08\x01\x00\x08\x01\x00\x02"
    b"\x0a\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x00\x00\x00\x00\x00\x00\x00"
    b"\x22\x22\x22\x22\x22\x22\x00\x00\x00"
)

class MHET_EPaper(framebuf.FrameBuffer):
    def __init__(self, spi, dc_pin, reset_pin, cs_pin, busy_pin):
        self.spi = spi
        self.rate = 4000000
        self.width = WIDTH
        self.height = HEIGHT
        self.dc_pin = dc_pin
        self.reset_pin = reset_pin
        self.cs_pin = cs_pin
        self.busy_pin = busy_pin
        
        self.buffer = bytearray((self.width * self.height + 7) >> 3)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_HLSB)
    
    # Wait for command execution
    def wait_ready(self):
        while self.busy_pin() == 1:
            sleep(0.001)
    
    # Hardware reset the controller
    def reset(self):
        self.reset_pin(1)
        sleep(0.2)
        self.reset_pin(0)
        sleep(0.005)
        self.reset_pin(1)
        sleep(0.2)
        
    # Send a command to the controller
    def send_cmd(self, cmd):
        self.spi.init(baudrate=self.rate)
        self.dc_pin(0)
        self.cs_pin(0)
        self.spi.write(bytearray([cmd]))
        self.cs_pin(1)

    # Send data to the controller
    def send_data(self, buf):
        self.spi.init(baudrate=self.rate)
        self.dc_pin(1)
        self.cs_pin(0)
        self.spi.write(buf)
        self.cs_pin(1)

    # Turn on the display
    def poweron(self):
        self.send_cmd(DISPLAY_UPDATE_CONTROL_2)
        self.send_data(bytearray([0xc7]))
        self.send_cmd(MASTER_ACTIVATION)
        self.wait_ready()
        
    # Turn off the display
    def poweroff(self):
        self.send_cmd(DEEP_SLEEP_MODE)
        self.send_data(bytearray([0x01]))
        
    # Waveform setting
    def set_lut(self):
        #print ('Set LUT')
        self.send_cmd( WRITE_LUT_REGISTER)
        self.send_data(bytearray(_LUT))
        self.send_cmd(DRIVE_OUTPUT_CONTROL)
        self.send_data(bytearray([0x22]))
        self.send_cmd(SET_GATE_VOLTAGE)
        self.send_data(bytearray([0x17]))
        self.send_cmd(SET_SOURCE_VOLTAGE)
        self.send_data(bytearray([0x41, 0x00, 0x32]))
        self.send_cmd(VCOM_VOLTAGE)
        self.send_data(bytearray([0x20]))
        #print ('LUT ok')
        
        
    # initializes the display
    def init_display(self):
        self.reset()
        self.wait_ready()
        self.send_cmd(SWRESET)
        self.wait_ready()
        for (cmd, param) in (
            (DRIVER_OUTPUT_CONTROL, (0xC7, 0x00, 0x01)),
            (DATA_ENTRY_MODE, (0x01,)),
            (SET_RAM_X_ADDRESS, (0, 24)),
            (SET_RAM_Y_ADDRESS, (199, 0, 0, 0)),
            (BORDER_WAVEFORM, (0x01,)),
            (UNDOC_1, (0x80,)),
            (DISPLAY_UPDATE_CONTROL_2, (0xB1,)),
            (MASTER_ACTIVATION, ()),
            (SET_RAM_X_CURSOR, (0,)),
            (SET_RAM_Y_CURSOR, (199, 0)),
        ):
            #print (cmd, param, len(param))
            self.send_cmd(cmd)
            if len(param) > 0:
                self.send_data(bytearray(param))
        self.wait_ready()
        self.set_lut()                
        self.fill(1)
        
    # Update the screen
    # THRE MUST BE A MINIMUM OF 3 MINUTES (180 SECONDS) BETWEEN CALLS!!!
    def show(self):
        self.send_cmd(WRITE_RAM_BLACK)
        self.send_data(self.buffer)
        self.poweron()
Por outro lado, a classe framebuf é bem limitada nos recursos de desenho. Dois pontos importantes:
  • O desenho de elipses é recente e não está disponível na versão "estável".
  • A escrita de texto é restrita a um fonte 8x8 que é inadequado para o display que estou usando
Comentando sobre isso no Twitter (me segue lá no @DQSoft), me apontaram para https://github.com/peterhinch/micropython-font-to-py, um projeto que inclui uma classe para escrita de texto em framebuf e um utilitário para converter fontes em um formato otimizado para uso com esta classe. Este projeto é companheiro do nano-gui (https://github.com/peterhinch/micropython-nano-gui). Este segundo projeto, além de ter classes para desenho de figuras mais sofisticadas, possui uma coleção de drivers, inclusive para e-paper.

O próximo passo vai ser estudar estes dois projetos e rever o meu driver tosco para ficar no mesmo nível dos que estão no nano-gui.

Nenhum comentário: