Linux

【Zynq7】実践6. SPI液晶をPythonで使う【液晶編3】

1.記事一覧

記事は複数回に分けて投稿します。XilinxのARM SoC (Zynq7020SoC)で動作する、YoctoベースのPetaLinuxを使わない素のLinuxをソースコードから構築する記事の続編です。

ZynqMP(arm64版)はこちら

エッジArm Linux構築編(Armv7)

エッジArm Linux実践編(Armv7)

2.【概要】Pythonでフルカラー液晶を使いたい

240×320ピクセルのSPI液晶を使う編の第3回になります。第1回第2回はこちら。

この液晶はaitendoのaitendoの2.2インチ320×240SPIフルカラー液晶です。液晶ドライバLSIはILITEK社のILI9328です。SPI通信のため動画のような高速描画向きではありませんが、フルカラーで解像度も高いのでちょっとしたUIを作るにはちょうどいいですね。

選択理由ですが、5年くらい前に買って部屋に放置してあったのを発掘した(笑)からです。今はもう少し新しいLSIがあるはずです。M5StackはILI9342あたりが搭載されていたと思います。

また、注意点になりますが、今回はユーザアプリで直接液晶を制御します。フレームバッファからデスクトップ画面を描画する、fbtft、flexfbカーネルモジュールを使用する方は今回はやりません。

2-1. ラズパイ向けの液晶ライブラリは移植しないと使えない

Webではラズパイ上でPythonで動かす作例が多く見つかりますが、Adafruit Industries社のドライバライブラリを使用している例が多いです。このライブラリはラズパイのIOのピンアサインまで抽象化されていて、いわゆるBSPレベルのものになっています。ラズパイ以外のArm Linuxボードでは動かせず、移植作業が必要そうでした。

ライブラリのソースを見てみたのですが、ボード定義部分が想像以上に複雑だったため、移植より新規に起こしたほうが早そうという結論になりました。

以上から、今回はILITEK社のILI9328向けのPythonライブラリを新規開発するところからやっていこうと思います。

今回使用する環境です。

開発PCUbuntu20.04
VSCode : Remote-SSH
ターゲットボードDigilent社製 Zybo z7-20
SoC:Xilinx社 Zynq7020 armv7hf (Cortex-A9×2、666MHz)
SDRAM:DDR3 1GB
u-boot:v2022.01
Linux Kernel: v5.15.0
RootFs:Ubuntu22.04 jammy armv7l
PythonPython3.9.2 (Debian:stable-slim armv7l コンテナ)
module: Pillow, py-spidev, Numpy
フォントUDEV Gothicフォントをサンプルコードで使用
https://github.com/yuru7/udev-gothic
SPIデバイス2.2インチ320×240フルカラー液晶
液晶コントローラ:ILI9328
aitendoモジュールキット
ロジックアナライザZeroplus社 LAP-C 16064 USB接続タイプ
簡易オシロスコープDSO nano v3 1ch 1MS/s

3.Python液晶ライブラリの仕様を決める

ILI9328についてざっくり説明します。

  • ILITEK製のTFT液晶ドライバLSI
  • インターフェースはパラレルIO(i80系8or16bit、RGB8or16or18bit) 、SPIから選択
  • 色深度はmax262K(18bit)フルカラー. 今回は16bitで使用する
  • サイズは240×320ピクセルでaitendoTFT液晶ボードは2.2inch
  • 画面の一部分に矩形領域描画する機能がある。矩形領域のみのデータ転送のため高速

データシートは以下になります。ILITEK社では問い合わせしないと取得できないようで、aitendoさんが公開されているリンクを張ります。
https://aitendo3.sakura.ne.jp/aitendo_data/product_img/lcd/tft2015/M022/ILI9328DS_V0.12.pdf

今回の仕様をざっくり決めます。

  • a. すべてPythonで実装する。C言語などの別言語呼び出しは使わない
  • b. 処理速度は気にしない
  • c. RaspberryPi以外でも使用できるようにボード依存はできるだけ無くす
  • d. インターフェースはAdafruitライブラリを参考にする

以下で詳しく説明します。

a.すべてPythonで実装する。C言語などの別言語呼び出しは使わない

今回は開発速度を速くするため、PythonモジュールをC言語実装するようなことはしないことにします。ただし、NumpyのようにC言語実装するのも面白そうなのでいつかやってみたいです。最近はRustでもモジュールが作れるようです。PyO3などなど。

b. 処理速度は気にしない

今回はRaspberryPiでしか使えないライブラリをZynqArmボードでも使えるようにするのが目的なので、処理速度は気にしないことにします。そもそもユーザ空間アプリで液晶を制御するので、パフォーマンスを気にしても限界があると思います。

c. RaspberryPi以外でも使用できるようにボード依存はできるだけ無くす

AdafruitのライブラリはWiringPiなどのボードIO を抽象化するライブラリの上で走るように作られています。今回はこれを使用しないようなライブラリを作ります。IO抽象化がない分、GPIO・SPI端子とソフトウェア上のインターフェースを紐付ける作業を開発者がやらないといけませんが、それが可能な技能レベルの人向けのライブラリになります。

具体的には

  • GPIO:該当するGPIO端子のSysfsインターフェース(valueファイル)を用意できる
  • SPI:py-spidev(spidevのPythonI/F)に必要なspidevドライバを用意できる

d. I/FはAdafruitライブラリのPillow-Imageの描画APIを採用

ライブラリのI/Fを決めるに当たり、Adafruitのライブラリには様々なAPIが用意されています。その中でもPillowのオブジェクトをそのまま渡して1画面表示するAPIが便利そうと思ったのでこれをサポートしたいと思います。PillowはPythonの有名な画像処理ライブラリの一つです。

コードの例としては以下のイメージです。

4.データシートを読む

データシートを確認していきます。

4-1. 配線の確認

以下が購入したaitendoのボード裏面です。画像左側がSPIのスルーホールになります。右側はSDカード関係なので無視してOKです。

データシート”4.Pin Descriptions”より、端子IM0/ID、IM1、IM2、IM3の電圧でインターフェースが決まります。このボードはすでにSPIで固定されていて、IM0/ID=0になっています。IDはこのあと出てきます。

SPIの端子は

  • D_SCK : Clock(データシート上では”SCL”と表記)
  • D_SDO : MISO
  • D_SDI : MOSI
  • D_CS : CS(ChipSelect)

その他端子は

  • D_RST : ILI9328 Reset
  • D_RS : ILI9328 Register Select (<- このあとこの端子は不要と判明します)
  • D_LED : バックライトLEDでGNDに接続すると点灯
  • D_Vcc : デフォルト3.3V

Vccはaitendoさんの商品ページにデフォルト3.3Vとあります。

4-2. SPIのタイミングチャート

続いてSPIの通信プロトコルの確認をしていきます。

データシート”6.3. Serial Peripheral Interface (SPI)”を見ていきます。

SPIモードはmode3ですね。StartByteのあとにデータを送るようです。
StartByteを見てみます。

“Transferred bits”がMSBから1,2,…と番号を振ってあるのが違和感がありますが、SPIはタイミングチャートを見る限り、MSB firstなので、Transferred bits=1はbit7になります。ここからはbitを読み替えてください。

bit7-2はDevice IDで固定です。”5-1. 配線の確認”でIDは0でしたので、0b011100xxになります。

bit1はRS(Register Select)信号です。”5-1. 配線の確認”で基板上にRS信号端子が出ていますが、SPI以外のインターフェースを使用するときに使用するので、RS端子は接続不要です。

  • RS=1はレジスタ及びGRAMにデータを読書き
  • RS=0はレジスタアドレスの書込み及びステータスの読み出し

GRAMはグラフィックRAMで画面に描画する各ピクセルのRGB値が入ります。また、シングルバッファなので、書き換え中の様子が画面に映ってしまします。

bit0はRead/Writeです。基本書込みしかしないので0固定になると思います。

StartByteはまとめると以下の値になります。

  • 0x70 : レジスタインデックスアドレス書込み
  • 0x72 : レジスタorGRAM書込み

続いてStartByte後のデータがレジスタやGRAMにどう書き込まれるかを確認します。

レジスタは基本16bitで最初の1byte目が上位byteに入り、2byte目が下位byteに格納されます。

GRAMは今回は16bitカラーを選択するので65Kカラーになります。ただし、GRAMは18bitサイズ(262Kカラー)になるので格納される値が16->18bitに変換されます。ちょっと複雑になりますが、16bitカラーはR=5bit、G=6bit、B=5bitになります。それを18bitカラーのR=6bit、G=6bit、B=6bitに拡張するのでRedとBlueが1bit分拡張されます。最下位bitを最上位bitの値で拡張しています。

最後に連続データ転送書込みの振る舞いについて確認します。

連続データ転送は可能で、そのままデータを送り続けるだけです。レジスタとGRAMのアドレスはインクリメントされます。

4-3. Application Notesでレジスタ初期値を確認する

ili9328のレジスタ一覧は”7.2 Instruction Descriptions”にあります。レジスタアドレス0x00~0xA5まで合計166あります。流石にデータシートから値を調べるのはしんどいので、アプリケーションノートを参考に決めます。
アプリケーションノートはこちら

各液晶メーカごとにサンプルコードが分かれていますが、ほとんど内容変わらないので、最初の”1.1CPT2.4″INITIAL CODE”を参考にしたいと思います。

void ILI9328_CPT24_Initial(void) の内容は

  • Reset LCD Driver
  • Start initial Sequence
  • Power On Sequence
  • Adjust the Gamma Curve
  • Set GRAM area
  • Partial Display Control
  • Panel Control

重要なのは次の2つです。

  • Reset LCD Driver
  • Start initial Sequence

“Reset LCD Driver”
リセット端子は今回GPIOで制御しますが、ウエイトをこの時間通りにします。

“Start initial Sequence” アドレス0x03、Entry Modeレジスタ
コメントに”BGR”とありますが、これはRGBのうち、RedとBlueを反転することを意味します。なぜだかわかりませんでしたが、これをやらないとRedとBlueが反転(RGB->BGR)してしまいました。
BGRはbit12です。

つづいてAM、I/D0、I/D1ビットです。これはGRAMに連続データ転送をしたときの、アドレスのインクリメントの仕方の設定です。サンプルコードの値のままで問題ありませんがこれが違うと鏡像反転したり、描画がめちゃくちゃになるので注意してください。

今回はAM=0、I/D1=1、I/D0=1にしています。水平は画面左から右、右端に到達したら1行下の左から右というGRAMのインクリメントになります。後述しますが、PillowのImageオブジェクトをNumpyで3次配列変換したあと、16bitRGBに変換します。このときのピクセル順がこの順で入ります。

ちなみにGRAMの水平・垂直アドレスの原点は写真の左上になります。

4-4. GRAMアクセス周りのレジスタ

最後にGRAMにアクセスするためのレジスタを確認します。

  • GRAM水平アドレスレジスタ(0x20) : GRAM読書き開始水平アドレス
  • GRAM垂直アドレスレジスタ(0x21) : GRAM読書き開始垂直アドレス
  • GRAM読書きレジスタ(0x22) : GRAMにアクセスする
  • HSAレジスタ(0x50) : 矩形領域描画の水平開始座標
  • HEAレジスタ(0x51) : 矩形領域描画の水平終了座標
  • VSAレジスタ(0x52) : 矩形領域描画の垂直開始座標
  • VEAレジスタ(0x53) : 矩形領域描画の垂直終了座標

5.Pythonライブラリ実装

必要な液晶デバイスの情報は出揃ったのでPythonで実装していきます。
使用するライブラリはインターフェースに関しては以下の通りになります。

  • SPI : py-spidevライブラリ
  • Reset : GPIO(Sysfs)をopen()、write()、close() <-ライブラリ不要

ここまでで、SPI液晶の初期化までは行けます。単色画像はリスト型配列に適当な値を詰めれば表示はできます。残る課題はPillowのImageオブジェクトをpy-spidevの提供する送信メソッドに渡せるデータに変換することです。

5-1. py-spidevのインターフェース仕様の確認

py-spidevのSPI送信メソッドについてまとめます。

writebytes()送信専用。リスト型を渡す。4096byte以上はwritebytes2()を使用する。
writebytes2()送信専用。Numpyのndarray型対応。4096byte以上は分割送信する。
xfer2()送受信可。リスト型を渡す。4096byte以上はxfer3()を使用する。
xfer3()送受信可。リスト型を渡す。4096byte以上は分割送信する。

ポイントはwritebytes2()がNumpy array型のオブジェクトを渡せることです。これは高速化が期待できると本家に書いてあります。list型は内部では線形リスト構造で実装してあると勝手に想像していますが、Numpy arrayはC言語の多次元配列です。これなら高速化が期待できるので、ぜひ使ってみましょう。

ちなみに4096byteという数値はspidevデバイスドライバ(/dev/spidev*.*)のデフォルトバッファサイズになります。ページサイズに合わせてあるのだと思います。spidevはカーネルモジュールの場合、modprobeし直すことで、バッファサイズを変えることができます。

Bash
$ cat /sys/module/spidev/parameters/bufsiz
4096
$ sudo rmmod spidev
$ sudo modprobe spidev bufsiz=8192
$ cat /sys/module/spidev/parameters/bufsiz
8192

ただし、再起動すると元に戻るので、永続化するには以下のようにconfファイルを作成します。

Bash
$ cat /etc/modprobe.d/spidev.conf
options spidev bufsiz=8192

spidevデバイスドライバのバッファサイズを増やすことで、py-spidev側の分割転送数を減らす、または無くす事ができ、高速化につながることや、割込み回数の減少でLinux側の負荷軽減が期待できます。

なぜ今この事を掘り下げるのかというと、このあと問題が起こるので、そこで伏線を回収します。

5-2. Pillow Image型から16bitRGB(65K)への変換方法をChatGPTに聞いてみた

ゴールはNumpy ndarray型(のピクセル配列)に決まりました。次に解決すべき課題はPillow Image型からゴールまでの道筋です。具体的には

  • Image型をNumpy ndarray型(何次元?)に変換する
  • 16bit(65K)カラーに変換する

が必要です。私はPillowもNumpyもそこまで使ったことがあるわけではないので、調査が難航しそうでした。そこでChatGPTに方法を聞いてみました。

質問
Pillowで240×320の画像を作成し、ピクセルデータをnumpyの配列に変換する方法を教えて下さい。 ただし、以下の条件とします。
・ピクセルデータの順番は画像の左上から右端の順にする。右端に到達したら、1つ下のピクセル行の左端から始まる順にする。
・numpy配列はRGBのピクセルデータを3byteから2byteに圧縮した値を格納します。RGBのピクセルデータはそれぞれred=5bit、green=6bit、blue=5bitに変換します。1byte目の下位3bitにはgreenの上位3bitを格納し、上位5bitにはredを格納します。2byte目の下位5bitにはblueの値を格納し、上位3bitはgreenの下位3bitを格納します。
・最終的なnumpy配列はuint8型の配列で一次元とする。
不明な点があれば質問してください。

回答(ChatGPT:GPT4)
このタスクでは、PythonのPIL (Pillow)ライブラリとnumpyライブラリを使って画像を操作します。以下に手順を示します。
まずは画像を生成し、その後でRGB値を変換します。そのために、まず適当な画像を生成します。ここでは単色の画像を生成することにします。

次に、この画像のRGB値を取得し、指定されたビット数に変換します。そして、それをnumpyの配列に格納します。

以上で、240×320の画像を生成し、RGBデータを特定の形式に変換してnumpy配列に格納する操作が完了しました。

なお、このコードでは生成される画像は単色のものとなります。もし特定のパターンや画像データを使いたい場合は、Image.newの代わりにImage.openを使って既存の画像ファイルを開く、あるいは他の方法で画像データを生成する必要があります。

これは…すごいですね。ほぼ完成されたコードを出してきました。こんなに簡単に記述できるのか?ImageオブジェクトってNumpyにそのまま変換できるんだ…といろいろ驚きました。

SPIで転送するロジックは以下のようにします。

  • スタートbyte(0x70)+レジスタアドレスをGRAMに設定(0x22)を送信
  • スタートbyte(0x72)+先程のNumpy 配列を送信

それでは実行してみましょう。

結果は…明らかに青過ぎる画面が出てきました(RGB=73:109:137のはず)。

最初のPillow Image()のコードで以下のように白画面生成に変更して実験します。
img = Image.new('RGB', (width, height), color = (255, 255, 255))

やはり見事な青です。残念ながら緑と赤が発色されていません。写真ではモアレ模様が写っていますが、実際は均一な色です。
実際の色はブルースクリーンのアレです(縁起が悪い)。

実はこのコード、ほぼ正しいです。Numpyを使い慣れている方やC言語使ってる人はすでにおかしな部分に気づいているのではないでしょうか。

5-3. ChatGPT先生にバグ修正を相談してみた

さて先生にバグを修正していただきましょう。「なんかよくわからないけど動かない」と言って面倒な方向に転がるとまずいので、怪しそうな部分をこちらで見つけて指摘してみます。

まず怪しいのは以下の記述です。

Python
r = (img_np[:,:,0] >> 3).astype(np.uint8) # Red: 5bit
g = (img_np[:,:,1] >> 2).astype(np.uint8) # Green: 6bit
b = (img_np[:,:,2] >> 3).astype(np.uint8) # Blue: 5bit
rgb565 = ((r << 11) | (g << 5) | b).astype(np.uint16)

img_npには白色を入れておきます。rgb565をprint()してみます。すると

[[255 255 255 … 255 255 255]
[255 255 255 … 255 255 255]
[255 255 255 … 255 255 255]
…
[255 255 255 … 255 255 255]
[255 255 255 … 255 255 255]
[255 255 255 … 255 255 255]]

Numpyのprint実装わかりやすくて優秀だなと関心しつつも、原因が見えてきました。
本来であればrgb565=[[0xFFFF 0xFFFF…が出るはずですが、
rgb565=[[0x00FF 0x00FF…になっているわけです。
若干緑が入りますが(下位3bit分)、ほぼ青になるわけですね。

なぜこうなるのでしょうか。rとgのビットシフト
(r << 11) | (g << 5) | b
これに問題があります。rとgの配列はastype(np.uint8)、つまり符号なし8bitで作成されています。
8bitの桁しか無いのですから、(r << 11)(g << 5)をすれば桁がオーバーフローします。

オーバーフロー後に.astype(np.uint16)をしても中身は入らないですよね。よって16bitのうち上位byteが空の0になります。

というわけで、バグの箇所をChatGPT先生に指摘して再度コード生成をお願いします。

質問
先程のコードが上手く動きませんでした。画像が青くなっています。
先程のコードの以下の記述で”r”と”g”のビットシフトで桁あふれが起こっていませんか? rgb565 = ((r << 11) | (g << 5) | b).astype(np.uint16)
修正ができるようでしたら再度、はじめからコードの出力をお願いします。

回答(ChatGPT:GPT4)
ご指摘いただきありがとうございます。ごめんなさい、ビットシフトにおける桁あふれについて誤って説明しました。
RGB565形式では、最初の8bitには赤と緑の上位2bit(合計5bit)、次の8bitには緑の下位4bitと青(合計8bit)が格納されます。
以下に修正版のコードを示します。

以上が、修正後のPythonコードになります。これで、指定されたビット配置でRGBデータを格納した一次元のnumpy配列が得られます。

さて、どうでしょうか?しっかり白色になりました! すごいぞ。

試しにPillow ImageDrawでシステムモニタっぽい絵を作ってみました。

完璧ですね…ChatGPT先生素晴らしい。こんなに早くAI相手にまともなレビューできる時代が来るとは思っていなかったです。

もう先生と呼ぶしかないですね。ここまで完璧なコード生成してくれるなら十分活用できます。今後も相棒確定です。

それとは別にNumpyのスカラー演算がここまで強力だとは思いませんでした。こんな簡単に書けるとは…いい経験になりました。

6.画像データ転送がなぜか途中で止まる問題

話は少しさかのぼります。先程のChatGPTに16bitカラー変換アルゴリズムを聞く前、SPI液晶の初期化と、GRAMに適当な値(単色緑)を1画面分転送できるかを試していたところ、ある問題にぶつかりました。

それはこんな感じです。目を凝らして見ないとわからないのですが、下から緑色が少しだけ転送されて残りが映りません。画面が白い部分は初期化後のGRAM初期値の色です。ちなみに液晶を逆さにしているため座標原点は右下です。下から上に順次データが転送されていきます。

6-1. SPI信号が出ているかオシロで見る

SPI信号が途中で止まったのかと思いきや、簡易オシロで見ると表示されなくなった後もしばらくデータは出ていました。見ているのはMOSIです。16bitのうち6bitがオール1なので、Duty比37.5%になります。波形もだいたい同じですね。データはあっていそうです。特に乱れもないです(あくまで目視の話で厳密にはわかりませんが)。

簡易オシロのため1chしかなく、CLKはサンプリング周波数不足(max1MHz)で見れないので、ちょっと妥協しています。

ちなみにロジアナで計測できればより確実で良いのですが、トレースバッファが少なく、1画面分のデータが取れません(安物なので…)。適当なGPIOを使って転送半ばからトリガをかければできなくもないですが、手間がかかるので後回しにしていました。

6-2. 映っている領域のデータ量を目視で概算する

とりあえずSPI信号が出続けているとなれば、SPI液晶側のバッファの異常かな?と思い、とりあえず、映っている画像部分のデータ量を目分量で概算します。とりあえずノギスで測りました。

液晶表示領域height=44.8mm
表示された領域height=1.4mm

1画面データ[byte]=1ピクセルあたりのデータ*width*height
 =2*240*320=153600=153[KB]
表示領域データ[byte]=1画面データ*表示された領域height/液晶表示領域height
 =153600*1.4/44.8=4800[byte]

表示されているデータ量は4KB強です。ここで、あることが頭によぎりました。

6-3. py-spidevライブラリで大容量データ転送時の落とし穴

そういえば、spidevデバイスドライバのデフォルトバッファサイズが4096byteでしたね。それ以上の転送はpy-spidevライブラリ内で分割して転送されます。懸念点は分割したフレーム(とりあえず分割された各データをフレームと呼びます)間でCS端子がインアクティブになっていないか調べます。

本当はプロトコルアナライザでSPIプロトコルにセッティングして測定したいのですが、4KBのデータ分のトレースバッファ容量が足りませんでした。そこで、GPIOで任意のタイミングでトリガを掛けるテストを書いてやってみます。

ロジアナのトリガ条件

テストコードvalue=0x01×5000byteをwritebytes2()で送信する。
GPIOで開始時↓終了時↑
トリガGPIOたち下がり時
トリガ用GPIO
SPI_CS
SPI_MOSI

転送の間はCSがLowアクティブになっているはずです。が、結果は10回ほど実施し、転送中にCS=Highになる瞬間をすべて捉えました。画像では見えにくいですが、白点線の丸の場所でCSのスパイク波形が出ています。

ILI9328のSPI転送はCS=low、スタートバイト送信、データ送信、CS=highで1回の通信になりますので、転送中にCS=highになるのは致命的です。

py-spidevでは分割転送が行われる4096byte以上のデータは使えなさそうです。そこで2つほど対策を考えてみました。

CSスパイク対策

  • spidevデバイスドライバのバッファサイズを大きくする(>153KB)
  • CS信号をGPIOで代用する

一つずつ検証してみます。

6-4. 対策1:spidevデバイスドライバのバッファサイズを大きくする

対策の1つ目、spidevデバイスドライバのバッファサイズを画面1枚分の153KBより大きめの160KBに変更してみます。”【Zynq7】実践4. LinuxでSPIを使う【液晶編1】”ではspidevをカーネルモジュールとしてビルドし直しましたので、変更が可能です。

160KBを確保するように変更します。

Bash
$ cat /sys/module/spidev/parameters/bufsiz
4096
$ sudo rmmod spidev
$ sudo modprobe spidev bufsiz=163840
$ cat /sys/module/spidev/parameters/bufsiz
163840

py-spidevはbufsizの値(/sys/module/spidev/parameters/bufsiz)を読んでから分割用のメモリを確保するので、転送データよりspidevのバッファサイズが大きければ分割しないはずです。

画面に緑単色画像を表示してみます。

今度は画面半分弱まで表示されてから止まりました…
気を取り直して映っている部分のデータサイズを概算してみます。

液晶表示領域height=44.8mm
表示された領域height=19.3mm

表示領域データ[byte]=1画面データ*表示された領域height/液晶表示領域height
 =153600*19.3/44.8=66.2*10^3[byte]

ほぼ64KBですね。偶然とは思えないのでどこかで制限がかかっているかもしれません。色々調査した結果、py-spidevに原因がありました。

原因のpy-spidevのコードを貼ります。

https://github.com/doceme/py-spidev/blob/master/spidev_module.c#L47
https://github.com/doceme/py-spidev/blob/master/spidev_module.c#L71

“/sys/module/spidev/parameters/bufsiz”からspidevのバッファサイズを読み出しますが、XFER3_MAX_BLOCK_SIZE=65535を超える場合はこの値に制限されます。
このマクロのコメントによると、

Largest block size for xfer3 – even if /sys/module/spidev/parameters/bufsiz allows bigger blocks, we won’t go above this value. As I understand, DMA is not used for anything bigger so why bother.
(xfer3の最大ブロックサイズ – /sys/module/spidev/parameters/bufsizがより大きなブロックを許可しても、この値以上にはしない。私の理解では、DMAはそれ以上の大きさには使用されないので、気にする必要はありません。)機械翻訳

だそうです。なるほどです。

ただし、py-spidevが悪いとは全く思いません。Zynq SPIのIPとpy-spidevの相性の問題でもありますし、調べた結果そもそもspidevで安易にバッファサイズを大きくして良いのかも問題でした。

その調べた結果というのが以下のspidevデバイスドライバのソースコードです。

https://github.com/torvalds/linux/blob/master/drivers/spi/spidev.c#L613
https://github.com/torvalds/linux/blob/master/drivers/spi/spidev.c#L621

spidevのデバイスファイル”/dev/spidev*.*”をopenしたときにこの処理がよばれます。
kmallocで送信と受信バッファをbufsizで指定したサイズで確保しています。

あまり、linux のメモリアロケータに詳しくないのですが、kmallocで160KB(40ページ分)の大きなメモリを確保して良いのか疑問に思ったので更に調べました。

6-5. 対策1つづき:kmallocで160KBのメモリを確保して良いのか?

kmallocとはLinuxカーネル内で使用できるメモリアロケータです。仮想記憶を確保できます。ページサイズ以上の領域を確保すると、物理アドレスも連続するという特徴があります。

物理アドレスが連続するということは40ページ分の連続した領域を確保するということです。linuxはページングで仮想記憶を使用しますので、稼働時間が長くなればなるほど、物理アドレスの空き領域はページ単位で断片化すると考えられます。40ページの連続した空きページがあればすぐ確保できますが、無ければページアウトが行われて確保しようとします。ページアウト中はプロセスはスリープになります(GFP_KERNEL)。

ここで言いたいのは、物理アドレスが連続したページの確保は大きさによってはスリープ時間が長くなる可能性があります。またページアウトのロジックに詳しくないのでわかりませんが、カーネルに負荷を与えそうです。このようなとき、複数ページのメモリ確保にはvmallocを使うことになると思います。vmallocは物理アドレスが連続しないので、SPIのIPでは扱えません。

さらに、ZynqSPIはおそらく使用していませんが、デバイスドライバによってはDMA転送を使うものもあります。この場合物理アドレスが連続した領域を使うのが普通です。kmallocで確保した領域の物理アドレスを取得するか、CMAによって連続物理アドレスの領域を確保するかなどの対応を行う必要があります。例外として、スキャッタギャザーDMAやIOMMUが利用できる場合は別です。

最後に、kmallocの割り当てられる最大サイズを調べようとしたのですが、よくわかりません。webで64KB、32MiB、4GBのような記述が見つかり、結局わかりませんでした。(どなたか教えてください)

ここまでのまとめ

  • 物理アドレスが連続でかつ多ページ領域のメモリ確保はリスクが大きい(kmalloc)
  • 160KB(40ページ)相当のkmallocはおそらく良くない
  • spidevのバッファサイズを増やす対応は今回は不採用

6-6. 対策2:CS信号をGPIOで代用する

2つ目の対応はZynq SPIのIPが制御するCS信号線を使わずにGPIOによるソフトウェア制御に変更します。前回のAXI-GPIO編でRS信号用を用意したのですが、不要とわかったのでこれをCS信号に代用します。

無事、1画面分映りました。

少々脱線しましたが、しっかり表示はできたようです。もう少し高級なロジアナと4chオシロがほしいなと思いました。ここまでやってから、6章のPillowのImageオブジェクトを16bitRGBに変換するロジックの実装編に続くのでChatGPT先生様々でした。先生いなかったらモチベが燃え尽きてたかもしれません。

7.作成したILI9328液晶ライブラリについて

作成した液晶ライブラリについて簡単に紹介したいと思います。

Github
https://github.com/kern-gt/simple-pytft/tree/main

特徴

  • Pythonユーザアプリ向け液晶ライブラリ
  • Raspberry Pi依存はありません
  • 16bitカラー(65K)対応
  • 画像処理ライブラリPillowのImageオブジェクトを表示可能
  • インターフェースはSPI(spidev)とGPIO2本(Sysfs)
  • Python依存モジュール:py-spidev、Numpy
  • 動画はパフォーマンス的に向いていません

サンプルコード内容

  • システムモニタ風画面+4方向表示
  • 矩形領域描画. 表示するのはmatplotlibグラフ

システムモニタ風画面ではUDEV Gothicフォントを使用しています。縦横すべての向きで表示するサンプルです。

矩形領域描画は画面の任意の場所に矩形領域の描画をするAPIのサンプルです。矩形領域内(グラフの部分)のみのデータ転送を行うため、比較的高速です。しかし矩形領域内に表示する絵としてmatplotlibのグラフを採用したら、レンダリング遅すぎて高速転送の意味がありませんでした。とは言いつつも作画がとても楽で移植も苦ではないので、作ってよかったと思います。

ABOUT ME
sh-goto
組込エンジニア. 最近の遊びはLinuxの低レイヤいじりとXilinxのZynqとFPGAを使った電子工作
関連記事