はじめに
AIDA64 Extreme や LCD4linux を使って、PC の CPU負荷 や 温度他 をモニターするときに使用する LCD(IPS液晶ディスプレイ)についての記事です。
ここでは、AX206LCD
と呼ぶことにします。
( AX206 とは、このディスプレイが使用しているLCDドライバーICの名称のようです )
- アリエクで入手したAX206LCDは、3.5インチ 480×320 ピクセルのもの
PC と AX206LCD は USB2.0 で接続し、本来の用途は、DPF(デジタルフォトフレーム)のようです。
- Mac にUSB接続したときの情報
$ 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コードです。
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)
超シンプルに表示できます。
draw()
1回あたり、0.6〜0.7秒で画面を書き換えることができました。
転送性能の考察
先の情報によると 最高12Mbps、効率を8割としてバイト換算すると、毎秒1.2MBの転送能力。
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バイト)。
約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コードを示す。
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
自分にとっては、こういうのも「電子工作」なんです。
以上