1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラズパイ5 2023年10月更新 bookworm ⑧ リベンジ pythonでグラフィック・ディスプレイを利用

Posted at

第3回の記事
  ラズパイ5 2023年10月更新 bookworm ③ pipが使えない! pythonでグラフィック・ディスプレイを利用
では、グラフィック・ディスプレイAdafruit 2.0" 320x240 Color IPS TFT Displayを、仮想環境で使うことができませんでした。
 ここでは、力ずくで、動かします。

仮想環境に入る

 envtftという仮想環境を作り、入ります。

yoshi@ras05:~ $ python -m venv envtft
yoshi@ras05:~ $ source envtft/bin/activate
(envtft) yoshi@ras05:~ $ 

 ライブラリ(モジュール)は空っぽです。

(envtft) yoshi@ras05:~ $ pip list
Package    Version
---------- -------
pip        23.0.1
setuptools 66.1.1

 この仮想環境で、st7789ライブラリをpipを使ってインストールします。

st7789ライブラリのインストール

 (envtft) yoshi@ras05:~ $ pip install st7789
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting st7789
  Using cached https://www.piwheels.org/simple/st7789/ST7789-0.0.4-py3-none-any.whl (8.4 kB)
Installing collected packages: st7789
Successfully installed st7789-0.0.4
envtft) yoshi@ras05:~ $ pip list
Package    Version
---------- -------
pip        23.0.1
setuptools 66.1.1
ST7789     0.0.4

 実行に必要な、pillow、numpy、spidev、RPi.GPIOをインストールします。

(envtft) yoshi@ras05:~ $ pip list
Package    Version
---------- -------
numpy      1.26.4
pillow     10.2.0
pip        23.0.1
RPi.GPIO   0.7.1
setuptools 66.1.1
spidev     3.6
ST7789     0.0.4

 次の表示プログラムst7789a.pyを動かします。

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

import ST7789

MESSAGE = "Hello World!"
print(MESSAGE)

#display_type == "dhmini":
disp = ST7789.ST7789(
   height=240,
   width=320,
   rotation=0,
   port=0,
   cs=0,
   dc=22,
   rst=25,
   backlight=None,
   spi_speed_hz=80 * 1000 * 1000,
)


# Initialize display.
disp.begin()

WIDTH = disp.width
HEIGHT = disp.height
img = Image.new('RGB', (WIDTH, HEIGHT), color=(255, 0, 0))

draw = ImageDraw.Draw(img)

font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 30)

draw.text((20,20),MESSAGE, font=font, fill=(255,255,255))

disp.display(img)

 エラーが出ます。

(envtft) yoshi@ras05:~ $ python st7789a.py
Hello World!
Traceback (most recent call last):
 File "/home/yoshi/st7789a.py", line 11, in <module>
   disp = ST7789.ST7789(
          ^^^^^^^^^^^^^^
 File "/home/yoshi/envtft/lib/python3.11/site-packages/ST7789/__init__.py", line 139, in __init__
   GPIO.setup(dc, GPIO.OUT)
RuntimeError: Cannot determine SOC peripheral base address

 このエラーは、ラズパイ5ではGPIOをアクセスするライブラリのRPi.GPIOが対応していないためです。ラズパイでは、gapiozeroライブラリをつかうべしという話が見つかります。そこで、/home/yoshi/envtft/lib/python3.11/site-packages/ST7789/__init__.pyのRPi.GPIOを利用しているところを、gapiozeroライブラリのコマンドで置き換えました。
 実行すると、二つのエラーが出ました。一つは、rpi-lgpioをインストールすると消えましたが、もう一つ残っています。
 GPIOの操作は、rst(リセット信号)、dc信号をON/OFFしているだけです。なので、subprocess.run()で、export /sys。。。/gpio1のようなGPIOを直にON/OFFしようと書き替えました。しかし、このexport操作は、ラズパイ5では使えなくなっていることがわかりました。
 使えるのはpinctrlですね。新しいshellで使えるコマンドです。GPIOの操作は、このコマンドに書き換えました。

__init__.py
# Copyright (c) 2014 Adafruit Industries
# Author: Tony DiCola
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import numbers
import time
import numpy as np

import spidev
# import RPi.GPIO as GPIO

import subprocess

__version__ = '0.0.4'

BG_SPI_CS_BACK = 0
BG_SPI_CS_FRONT = 1

SPI_CLOCK_HZ = 16000000

ST7789_NOP = 0x00
ST7789_SWRESET = 0x01
ST7789_RDDID = 0x04
ST7789_RDDST = 0x09

ST7789_SLPIN = 0x10
ST7789_SLPOUT = 0x11
ST7789_PTLON = 0x12
ST7789_NORON = 0x13

ST7789_INVOFF = 0x20
ST7789_INVON = 0x21
ST7789_DISPOFF = 0x28
ST7789_DISPON = 0x29

ST7789_CASET = 0x2A
ST7789_RASET = 0x2B
ST7789_RAMWR = 0x2C
ST7789_RAMRD = 0x2E

ST7789_PTLAR = 0x30
ST7789_MADCTL = 0x36
ST7789_COLMOD = 0x3A

ST7789_FRMCTR1 = 0xB1
ST7789_FRMCTR2 = 0xB2
ST7789_FRMCTR3 = 0xB3
ST7789_INVCTR = 0xB4
ST7789_DISSET5 = 0xB6

ST7789_GCTRL = 0xB7
ST7789_GTADJ = 0xB8
ST7789_VCOMS = 0xBB

ST7789_LCMCTRL = 0xC0
ST7789_IDSET = 0xC1
ST7789_VDVVRHEN = 0xC2
ST7789_VRHS = 0xC3
ST7789_VDVS = 0xC4
ST7789_VMCTR1 = 0xC5
ST7789_FRCTRL2 = 0xC6
ST7789_CABCCTRL = 0xC7

ST7789_RDID1 = 0xDA
ST7789_RDID2 = 0xDB
ST7789_RDID3 = 0xDC
ST7789_RDID4 = 0xDD

ST7789_GMCTRP1 = 0xE0
ST7789_GMCTRN1 = 0xE1

ST7789_PWCTR6 = 0xFC


class ST7789(object):
    """Representation of an ST7789 TFT LCD."""

    def __init__(self, port, cs, dc, backlight=None, rst=None, width=240,
                 height=240, rotation=90, invert=True, spi_speed_hz=4000000,
                 offset_left=0,
                 offset_top=0):
        """Create an instance of the display using SPI communication.

        Must provide the GPIO pin number for the D/C pin and the SPI driver.

        Can optionally provide the GPIO pin number for the reset pin as the rst parameter.

        :param port: SPI port number
        :param cs: SPI chip-select number (0 or 1 for BCM
        :param backlight: Pin for controlling backlight
        :param rst: Reset pin for ST7789
        :param width: Width of display connected to ST7789
        :param height: Height of display connected to ST7789
        :param rotation: Rotation of display connected to ST7789
        :param invert: Invert display
        :param spi_speed_hz: SPI speed (in Hz)

        """
        if rotation not in [0, 90, 180, 270]:
            raise ValueError("Invalid rotation {}".format(rotation))

        if width != height and rotation in [90, 270]:
            raise ValueError("Invalid rotation {} for {}x{} resolution".format(rotation, width, height))

        # GPIO.setwarnings(False)
        # GPIO.setmode(GPIO.BCM)

        self._spi = spidev.SpiDev(port, cs)
        self._spi.mode = 0
        self._spi.lsbfirst = False
        self._spi.max_speed_hz = spi_speed_hz

        self._dc = dc
        self._rst = rst
        self._width = width
        self._height = height
        self._rotation = rotation
        self._invert = invert

        self._offset_left = offset_left
        self._offset_top = offset_top

        # Set DC as output.
        # GPIO.setup(dc, GPIO.OUT)

        # Setup backlight as output (if provided).
        self._backlight = backlight
        if backlight is not None:
            # GPIO.setup(backlight, GPIO.OUT)
            # GPIO.output(backlight, GPIO.LOW)
            # time.sleep(0.1)
            # GPIO.output(backlight, GPIO.HIGH)
            pass

        # Setup reset as output (if provided).
        if rst is not None:
            # GPIO.setup(rst, GPIO.OUT)
            pass

        self.reset()
        self._init()

    def send(self, data, is_data=True, chunk_size=4096):
        """Write a byte or array of bytes to the display. Is_data parameter
        controls if byte should be interpreted as display data (True) or command
        data (False).  Chunk_size is an optional size of bytes to write in a
        single SPI transaction, with a default of 4096.
        """
        # Set DC low for command, high for data.
        # GPIO.output(self._dc, is_data)
        if is_data:
            isdata='dh'
        else:
            isdata='dl'
        msg = 'pinctrl set '+ str(self._dc) + ' op ' + isdata
        subprocess.run(msg,shell=True)

        # Convert scalar argument to list so either can be passed as parameter.
        if isinstance(data, numbers.Number):
            data = [data & 0xFF]
        # Write data a chunk at a time.
        for start in range(0, len(data), chunk_size):
            end = min(start + chunk_size, len(data))
            self._spi.xfer(data[start:end])

    def set_backlight(self, value):
        """Set the backlight on/off."""
        if self._backlight is not None:
            # GPIO.output(self._backlight, value)
            pass

    @property
    def width(self):
        return self._width if self._rotation == 0 or self._rotation == 180 else self._height

    @property
    def height(self):
        return self._height if self._rotation == 0 or self._rotation == 180 else self._width

    def command(self, data):
        """Write a byte or array of bytes to the display as command data."""
        self.send(data, False)

    def data(self, data):
        """Write a byte or array of bytes to the display as display data."""
        self.send(data, True)

    def reset(self):
        print("Reset the display, if reset pin is connected.")
        if self._rst is not None:
            msg = 'pinctrl set '+ str(self._rst) + ' op dh'
            # msg = 'pinctrl set 25 op dh'
            subprocess.run(msg,shell=True)
            time.sleep(0.500)
            msg = 'pinctrl set '+ str(self._rst) + ' op dl'
            # msg = 'pinctrl set 25 op dl'
            subprocess.run(msg,shell=True)
            time.sleep(0.500)
            msg = 'pinctrl set '+ str(self._rst) + ' op dh'
            # msg = 'pinctrl set 25 op dh'
            subprocess.run(msg,shell=True)
            time.sleep(0.500)

    def _init(self):
        # Initialize the display.

        self.command(ST7789_SWRESET)    # Software reset
        time.sleep(0.150)               # delay 150 ms

        self.command(ST7789_MADCTL)
        self.data(0x70)

        self.command(ST7789_FRMCTR2)    # Frame rate ctrl - idle mode
        self.data(0x0C)
        self.data(0x0C)
        self.data(0x00)
        self.data(0x33)
        self.data(0x33)

        self.command(ST7789_COLMOD)
        self.data(0x05)

        self.command(ST7789_GCTRL)
        self.data(0x14)

        self.command(ST7789_VCOMS)
        self.data(0x37)

        self.command(ST7789_LCMCTRL)    # Power control
        self.data(0x2C)

        self.command(ST7789_VDVVRHEN)   # Power control
        self.data(0x01)

        self.command(ST7789_VRHS)       # Power control
        self.data(0x12)

        self.command(ST7789_VDVS)       # Power control
        self.data(0x20)

        self.command(0xD0)
        self.data(0xA4)
        self.data(0xA1)

        self.command(ST7789_FRCTRL2)
        self.data(0x0F)

        self.command(ST7789_GMCTRP1)    # Set Gamma
        self.data(0xD0)
        self.data(0x04)
        self.data(0x0D)
        self.data(0x11)
        self.data(0x13)
        self.data(0x2B)
        self.data(0x3F)
        self.data(0x54)
        self.data(0x4C)
        self.data(0x18)
        self.data(0x0D)
        self.data(0x0B)
        self.data(0x1F)
        self.data(0x23)

        self.command(ST7789_GMCTRN1)    # Set Gamma
        self.data(0xD0)
        self.data(0x04)
        self.data(0x0C)
        self.data(0x11)
        self.data(0x13)
        self.data(0x2C)
        self.data(0x3F)
        self.data(0x44)
        self.data(0x51)
        self.data(0x2F)
        self.data(0x1F)
        self.data(0x1F)
        self.data(0x20)
        self.data(0x23)

        if self._invert:
            self.command(ST7789_INVON)   # Invert display
        else:
            self.command(ST7789_INVOFF)  # Don't invert display

        self.command(ST7789_SLPOUT)

        self.command(ST7789_DISPON)     # Display on
        time.sleep(0.100)               # 100 ms

    def begin(self):
        """Set up the display

        Deprecated. Included in __init__.

        """
        pass

    def set_window(self, x0=0, y0=0, x1=None, y1=None):
        """Set the pixel address window for proceeding drawing commands. x0 and
        x1 should define the minimum and maximum x pixel bounds.  y0 and y1
        should define the minimum and maximum y pixel bound.  If no parameters
        are specified the default will be to update the entire display from 0,0
        to width-1,height-1.
        """
        if x1 is None:
            x1 = self._width - 1

        if y1 is None:
            y1 = self._height - 1

        y0 += self._offset_top
        y1 += self._offset_top

        x0 += self._offset_left
        x1 += self._offset_left

        self.command(ST7789_CASET)       # Column addr set
        self.data(x0 >> 8)
        self.data(x0 & 0xFF)             # XSTART
        self.data(x1 >> 8)
        self.data(x1 & 0xFF)             # XEND
        self.command(ST7789_RASET)       # Row addr set
        self.data(y0 >> 8)
        self.data(y0 & 0xFF)             # YSTART
        self.data(y1 >> 8)
        self.data(y1 & 0xFF)             # YEND
        self.command(ST7789_RAMWR)       # write to RAM

    def display(self, image):
        """Write the provided image to the hardware.

        :param image: Should be RGB format and the same dimensions as the display hardware.

        """
        # Set address bounds to entire display.
        self.set_window()

        # Convert image to 16bit RGB565 format and
        # flatten into bytes.
        pixelbytes = self.image_to_data(image, self._rotation)

        # Write data to hardware.
        for i in range(0, len(pixelbytes), 4096):
            self.data(pixelbytes[i:i + 4096])

    def image_to_data(self, image, rotation=0):
        if not isinstance(image, np.ndarray):
            image = np.array(image.convert('RGB'))

        # Rotate the image
        pb = np.rot90(image, rotation // 90).astype('uint16')

        # Mask and shift the 888 RGB into 565 RGB
        red   = (pb[..., [0]] & 0xf8) << 8
        green = (pb[..., [1]] & 0xfc) << 3
        blue  = (pb[..., [2]] & 0xf8) >> 3

        # Stick 'em together
        result = red | green | blue

        # Output the raw bytes
        return result.byteswap().tobytes()

 実行します。

(envtft) yoshi@ras05:~ $ python st7789a.py
Hello World!

 めでたく動きました。

IMGP2127.png

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?