5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

電子工作Advent Calendar 2024

Day 10

AX206 LCD について

Last updated at Posted at 2024-12-09

はじめに

AIDA64 ExtremeLCD4linux を使って、PC の CPU負荷 や 温度他 をモニターするときに使用する LCD(IPS液晶ディスプレイ)についての記事です。

ここでは、AX206LCDと呼ぶことにします。
AX206 とは、このディスプレイが使用しているLCDドライバーICの名称のようです

  • アリエクで入手したAX206LCDは、3.5インチ 480×320 ピクセルのもの

LCD-3-5-C-CPU-pu-USB-C.png

PC と AX206LCD は USB2.0 で接続し、本来の用途は、DPF(デジタルフォトフレーム)のようです。

  • Mac にUSB接続したときの情報

usb0.png

$ lsusb 
Bus 000 Device 007: ID 1908:0102 1908 USB-Display  Serial: WCH32

この手の多くのLCDドライバーICで採用されている RGB565 カラー形式で、6万5千色を表示できます。

USBデバイスであれば、pyusb を使って表示させることができますが、サイズ調整やRGB565変換等の処理が必要なため、AX206LCD クラスを作って 簡単に表示できるようにします。

Python でアクセスできるため、Pillow を使って好みの画像内容を編集し、それを簡単にAX206LCDに表示させることができるようになります。



その前に、

LCD4linux のソースコードと、LCD4linux や AIDA64 と 当LCD が実際に入出力した USBデータのキャプチャー情報から解析した、AX206LCDにアクセスするための情報をまとめておきます。

USB Mass Storage Class - Bulk Only Transport

このLCDは USB Mass Storage Class - Bulk Only Transport プロトコルでアクセスします。

Endpoint

Bulk 転送に使用するエンドポイントは、次の通り。

  • PC → LCD : 0x01
  • LCD → PC : 0x81

CBW (Command Block Wrapper)

CBW(31バイト)の内容は、次の通り。

offset パラメタ 内容
0-3 dCBWSignature 0x55 0x53 0x42 0x43 ("USBC")
4-7 dCBWTag 0xde 0xad 0xbe 0xef
8-11 dCBWDataTransferLength データ転送長 (little endian)
12 bmCBWFlags 0x80 : Data In (LCD → PC)
0x00 : Data Out (PC → LCD)
13 bCBWLUN 0x00
14 bCBWCBLength 0x10 (CBWCB長)
15-30 CBWCB 後述

CSW (Command Status Wrapper)

CSW(13バイト)の内容は、次の通り。
(LCD4linux では、ACK と表現している)

offset パラメタ 内容
0-3 dCSWSignature 0x55 0x53 0x42 0x53 ("USBS")
4-7 dCSWTag 0xde 0xad 0xbe 0xef
8-11 dCSWDataResidue 0x00 0x00 0x00 0x00 0x00
12 bCSWStatus 0x00 : Command Passed (good status)
0x01 : Command Failed
0x02 : Phase Error

CBWCB (Command Block Wrapper - Command Block)

次の3種類を確認した

1. LCDサイズ取得

デバイスにLCDのサイズ(width・height)を問い合わせる。

offset パラメタ 内容
0 0xcd
1-4 0x00 0x00 0x00 0x00
5 0x02 : get LCD parameters
6-15 0x00 --- 0x00

上記データをAX206LCDに送信すると、次の5バイトのデータが デバイスから送り返される。

offset パラメタ 内容
0-1 width 0xe0 0x01(0x01e0:480)
2-3 height 0x40 0x01(0x0140:320)
4 0xff

2. バックライト設定

LCDのバックライトの明るさを設定する

offset パラメタ 内容
0 0xcd
1-4 0x06 0x01 0x01 0x00
5 0x00 〜 0x07 : set LCD backlight
6-15 0x00 --- 0x00

3. 画像データ転送

表示する画像データを AX206LCDに転送する

offset パラメタ 内容
0 0xcd
1-4 0x00 0x00 0x00 0x00
5-6 0x06 0x12
7-8 x0 0x00 0x00(0x0000:0)
9-10 y0 0x00 0x00(0x0000:0)
11-12 x1 0xdf 0x01(0x01df:479)
13-14 y1 0x3f 0x01(0x013f:319)
15 0x00

これに続き、(x0, y0)〜(x1, y1) の RGB565データを転送する。

(ただし、(0, 0)〜(479, 319)以外(一部領域指定)のデータ転送は、当LCDは無応答となる。)

AX206LCDクラス の 使用例

次に AX206LCDクラス の 使用例を示します。

商品ページにあったサンプル画像を、順に表示させる pythonコードです。

sample.jpg

import time
from AX206LCD import AX206LCD
from PIL import Image

lcd = AX206LCD()
lcd.setbacklight(2)

files = ['sample1', 'sample2', 'sample3']
while True:
    for file in files:
        image = Image.open(f'./{file}.png')
        lcd.draw(image)
        time.sleep(5)

超シンプルに表示できます。

IMG_sample1.png IMG_sample2.png IMG_sample3.png

draw()1回あたり、0.6〜0.7秒で画面を書き換えることができました。

転送性能の考察

先の情報によると 最高12Mbps、効率を8割としてバイト換算すると、毎秒1.2MBの転送能力。

usb1.png

480x320のRGB565のイメージデータは 307,200バイト(300KB)で、単純な割り算だと 0.244秒となる。

しかし、USBプロトコルのやりとりが何往復も発生することと、LCD内の画像処理時間が多くの時間を占めると想定する。

次のスクショの黄色線の箇所での 0.517秒の内訳は、
・262171バイトのデータ転送に 0.208秒
・残りの 0.309秒がLCD内部処理
と想定される。

なお、307,200バイトのイメージデータは、262,171バイトと 45,083バイトの2回に分けて転送されている(含む、USBプロトコルヘッダー27バイト)。

wireshake.png

約1.5fpsのため、真にリアルタイムな更新には向きませんが、リソースモニターや現在時刻表示(時計)程度なら十分かもしれません。

なお、LCD4Linux で実装されている『画面領域の一部矩形の書き換え』は、当LCDは機能しませんでした(コマンドを送信しても応答が来ない、画面も書き換わらない)。

AX206LCDクラス の 使い方

  • from AX206LCD import AX206LCD
    AX206LCDクラスをインポート
     

  • lcd = AX206LCD()
    AX206LCDの初期化を行う。その前にAX206LCDを接続しておくこと(複数台の同時接続には未対応)。
    初期化でエラーが起きる場合は、USBケーブルの抜き差しを行ってみてください
     

  • lcd.setbacklight(brightness)
    LCDのバックライトの明るさを、0〜7 で指定する。LCDの初期値は 7、
    0 を指定するとバックライトは消灯して画像は見えない
     

  • lcd.draw(image)
    Imageを当クラス内部で480x320にリサイズして画面に表示する。
    横縦比が3:2でない画像の場合は、画像を中央に配置し、余白を黒で表示する。基本は呼ぶ側では画像サイズを気にする必要は無い

AX206LCDクラス

AX206LCDクラス の pythonコードを示す。

AX206LCD.py
import io
import usb.core
import usb.util
from struct import pack, unpack
from PIL import Image, ImageOps

class AX206LCD_Base:
    _AX206_VID = 0x1908
    _AX206_PID = 0x0102
    _black = (0, 0, 0)

class AX206LCD(AX206LCD_Base):
    def __init__(self, debug=False):
        self.width = 0
        self.height = 0
        self.dev = None
        self.DIR_IN = "IN"
        self.DIR_OUT = "OUT"
        self.__debug = debug
        self.__counter = 0 #for debug

        #open device
        dev = usb.core.find(idVendor=AX206LCD_Base._AX206_VID, idProduct=AX206LCD_Base._AX206_PID)
        assert dev is not None, "AX206LCD not found. It may not be connected."
        self.dev = dev

        dev.set_configuration()

        cmd = b'\xcd\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        buf = bytearray(5)
        assert self.__wrap_scsi(cmd, self.DIR_IN, buf) == 0, "device open error reading LCD dimensions"

        self.width, self.height = unpack('<HH', buf[0:4])
        print(f"AX206LCD: got LCD dimensions: {self.width}x{self.height}")

    def __del__(self):
        if self.dev is None: return
        usb.util.dispose_resources(self.dev)
        self.dev = None

    def setbacklight(self, brightness=1):
        assert 0 <= brightness <= 7, "must be value is 0...7"

        cmd = bytearray(b'\xcd\x00\x00\x00\x00\x06\x01\x01\x00\xff\x00\x00\x00\x00\x00\x00')
        cmd[9] = brightness
        assert self.__wrap_scsi(cmd, self.DIR_OUT, None) == 0, f"setbacklight failer. value: {brightness}"

    def __resize(self, image):
        x_ratio = self.width / image.width
        y_ratio = self.height / image.height
        if x_ratio < y_ratio:
            resize_size = (self.width, round(image.height * x_ratio))
        else:
            resize_size = (round(image.width * y_ratio), self.height)

        image = image.resize(resize_size)
        newImage = Image.new("RGBA", (self.width, self.height), AX206LCD_Base._black)
        #centering
        x, y = (self.width - image.size[0]) // 2, (self.height - image.size[1]) // 2
        newImage.paste(image, (x, y))
        return newImage

    def clear(self, color=AX206LCD_Base._black):
        #RGBA to RGB565
        r, g, b = color
        rgb565 = bytes([(((r & 0xf8)) | ((g & 0xe0) >> 5)), (((g & 0x1c) << 3 ) | ((b & 0xf8) >> 3))])

        out_size = self.width * self.height * 2
        out_img = bytearray(out_size)
        for n in range(0, out_size, 2): out_img[n: n+2] = rgb565

        cmd = bytearray(b'\xcd\x00\x00\x00\x00\x06\x12\xff\xff\xff\xff\xff\xff\xff\xff\x00')
        cmd[7:15] = pack("<HHHH", 0, 0, self.width - 1, self.height - 1)
        assert self.__wrap_scsi(cmd, self.DIR_OUT, out_img) == 0, f"draw failer. size: {len(out_img)}"

    def draw(self, image):
        image = self.__resize(image)
        image = ImageOps.flip(image) # horizontal flip

        img_bytes = io.BytesIO()
        image.save(img_bytes, format="BMP")
        image_bytes = img_bytes.getvalue()[54:] #skip BMP header (54bytes)
        width, height = image.size
        byteperpixel = len(image_bytes) // (width * height)
        out_size = width * height * 2
        out_img = bytearray(out_size)
        for n in range(0, out_size, 2):
            m = n // 2 * byteperpixel
            #RGBA to RGB565
            out_img[n + 0] = (((image_bytes[m + 2] & 0xf8)      ) | ((image_bytes[m + 1] & 0xe0) >> 5))
            out_img[n + 1] = (((image_bytes[m + 1] & 0x1c) << 3 ) | ((image_bytes[m + 0] & 0xf8) >> 3))

        cmd = bytearray(b'\xcd\x00\x00\x00\x00\x06\x12\xff\xff\xff\xff\xff\xff\xff\xff\x00')
        cmd[7:15] = pack("<HHHH", 0, 0, width - 1, height - 1)
        assert self.__wrap_scsi(cmd, self.DIR_OUT, out_img) == 0, f"draw failer. size: {len(out_img)}"

    def __dump(self, title, buf, readlen=0):
        if not self.__debug: return
        
        length = 0 if buf is None else len(buf)
        str = '' if readlen==0 else f', read-len:{readlen}'
        print(f"#{self.__counter} <{title}> buf-len: {length}{str}")
        if buf is None: return
        
        if length > 64: length = 64
        for n in range(length):
            if n > 0 and n % 16 == 0: print()
            d = buf[n]
            if 0x20 <= d <= 0x7f:
                c = f"[{chr(d)}]{'{:02x}'.format(d)}"
            else:
                c = f"{'{:02x}'.format(d)}"
            print(c, end=' ')
        print()

    def __wrap_scsi(self, cmd, dir, buf):
        if self.__debug:
            self.__counter += 1
            print(f"#{self.__counter} wrap_scsi cmd:{cmd} dir:{dir}, buf:{'None' if buf is None else f'size:{len(buf)}'}")

        cbw = bytearray(b'USBC\xde\xad\xbe\xef\x00\x00\x00\x00\x00\x00\x10')
        cbw[14] = len(cmd)
        if buf is not None:
            cbw[8:12] = pack("<L", len(buf))

        out = cbw + cmd
        self.__dump("cmd bulk write", out)
        ret = self.dev.write(0x1, out, 1000)
        assert ret is not None, f"{self.__counter} cmd bulk write failed. cmd:{out}"

        if dir == self.DIR_OUT and buf is not None:
            self.__dump("buf bulk write", buf)
            ret = self.dev.write(0x1, buf, 3000)
            assert ret is not None, f"{self.__counter} cmd bulk write failed. buf len:{len(buf)}"

        elif dir == self.DIR_IN and buf is not None:
            buf_len = len(buf)
            self.__dump("cmd bulk reading", None, buf_len)
            ret = self.dev.read(0x81, buf_len, 4000)
            assert ret is not None, f"{self.__counter} cmd bulk read failed."
            recv = bytearray(ret)
            assert len(recv) == buf_len, f"{self.__counter} cmd bulk read length unmatch. {buf_len}:{len(recv)}"
            buf[0:buf_len] = recv
            self.__dump("cmd bulk readed", buf)

        #get CSW as ACK response
        self.__dump("ack bulk reading", None)
        ret = self.dev.read(0x81, 13, 5000) # CSW size is 13
        assert ret is not None, f"{self.__counter} ack read failed."
        csw = bytearray(ret)
        self.__dump("ack bulk read", csw)
        assert len(csw) == 13, f"{self.__counter} ack read length unmatch. 13:{len(csw)}"
        assert csw[:4] == b'USBS', f"{self.__counter} NO ACK. {csw[:4]}"
        
        return csw[12] # bCSWStatus

自分にとっては、こういうのも「電子工作」なんです。



以上

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?