WindowsのDLLには2種類あります。一つは昔から使われているCやC++のDLLで、もう一つは.NETとかで使われているDLLです。Pythonにはctypesというライブラリがあり、これを使うとCやC++のDLLを利用することができます。
今日はctypesを使って特電Artix-7ボードで使われているUSB3.0 APIをPythonからアクセスできるようにしてみました。
まず、ctypesを使ってDLLを読み込むには
import ctypes
fx3 = ctypes.WinDLL("./tkusbfx3.dll")
と書きます。PythonのファイルとDLLが同じディレクトリにある場合は./で相対パス指定しないといけないようです。
さて、CのDLLには「型」の情報が含まれていません。そこで、Pythonからアクセスするには引数の型と戻り値の型を指定してあげなければなりません。Pythonでは型は動的に付けられますが、ctypesにはc_uint16やc_uint32といった型が用意されているので、形無し言語が苦手な私のような人でも安心です。
このTKUSBFX3Openという関数は、開いたUSBデバイスのベンダIDやプロダクトID、デバイス名など複数の戻り値をポインタにいれて返してくるので、
bool TKUSBFX3Open(int num, unsigned short *vid, unsigned short *pid, char *DeviceName, int MaxDevnameLength);
というプロトタイプ宣言なのですが、ここでいきなり難題にぶちあたりました。
CのDLLからポインタで書き込まれる戻り値をどうやって受け取ればいいのでしょうか?
文字列ポインタはc_char_pで受け取ればよいのはググればすぐにわかりますが、uint16の値をポインタ渡しで受け取る方法はなかなか見つかりません。
答えは、
fx3.TKUSBFX3Open.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_uint16), ctypes.POINTER(ctypes.c_uint16), ctypes.c_char_p, ctypes.c_int)
fx3.TKUSBFX3Open.restype = ctypes.c_bool
でした。
uint16 *で戻ってくる値は、ctypes.POINTER(ctypes.c_uint16)と書けばいいようです。
いちいちctypes.を付けるのが面倒なのですが、Python初心者なので詳しくはわかりませんが、importの代わりにfrom ctypes import *にすればPOINTER(c_uint16)と書けるのではないかと思います。
さて、この関数を呼び出してオープンする部分は、
vid = ctypes.c_uint16()
pid = ctypes.c_uint16()
buf = ctypes.create_string_buffer(100)
status = fx3.TKUSBFX3Open(index, vid, pid, buf, 100)
と書きます。
ctypesの中にあるc_uint16というクラスを使うことで明示的に型が指定できます。整数なのにわざわざクラスでコンストラクタを呼ばなければならないのが大げさな感じがしますが仕方がありません。それから、関数を呼び出すときに参照を取らなくてもよいのかもしれません。
そして、文字列を配列に格納して返す関数を呼び出すには、create_string_buffer(100)というのを使って配列を用意しておきます。これはchar *型のmallocに相当するものだと理解しています。DLLで文字列を操作することはよくあるのでcreate_string_bufferはよく使うことになるのかなと思います。
create_string_bufferを使えばmallocみたいなことができるので、文字列以外にもなんでも使えるのではないかと思ったのですが、どうやらこれはc_char_pと一緒に使うらしいのです。c_char_pは\0で終わる文字列なので、バイナリのバッファで使うのはあまりよくないのではないかという気がしてきました。
USBに大量に大量のバイナリデータを送受信するようなアプリではどうすればよいかを考えてます。
int USBWriteData(unsigned long addr, unsigned char *data, int len, unsigned short flag);
という関数があります。これをctypesを使って引数と戻り値の型を書くと、
fx3.USBWriteData.argtypes = (ctypes.c_uint32, ctypes.POINTER(ctypes.c_ubyte), ctypes.c_int, ctypes.c_uint16)
fx3.USBWriteData.restype = ctypes.c_int
になりました。
unsigned char * → POINTER(c_ubyte)
と変換すればよいようです。
送信するデータのバッファをcreate_string_bufferで作ると、mutableではないと言ってエラーになります。
初めて知ったのですが、Pythonでは変更できない変数やオブジェクトがあるようです。
変更可能なバイト列バッファを作るには
wbuf = bytearray(INOUT_BUFFER_SIZE) # ミュータブルなバイト配列
for i in range(length):
wbuf[i] = int(random.random() * 256)
としておきます。
そして、実際に送信するところでは、
fx3.USBWriteData(addr , (ctypes.c_ubyte * length).from_buffer(buf) , min(length,len(buf)) , flag)
とします。
ここで謎の記法が出てきました。
(ctypes.c_ubyte * len(wbuf)).from_buffer(wbuf)
とは何かということです。本当にPythonのことはよくわからないのですが、ctypes.c_ubyteの型がwbufの長さの分だけ並べた配列という新たな型を作っているようです。そして、そのオブジェクトを作る際に既存のbufを参照して使うということであるようです。
他にも、
(ctypes.c_ubyte * len(wbuf))(*wbuf)
という書き方をすることもできるのですが、wbufという配列の実体を作ってそれを(ctypes.c_ubyte * len(wbuf))にキャストするようなので、実行に非常に時間がかかります。16MBytesの配列で2秒くらいかかるようです。
だから、from_bufferを使うのがよいのでしょう。from_bufferを使えばC/C++で書いたのと同じ速さで送受信ができたので、おそらくデータのコピーは発生していません。
このような感じでArtix-7ボードのEZ-USB FX3をPythonからアクセスして、C/C++と同じ速度でデータの入出力ができるようになりました。
C/C++のときと比べて全く遜色のない速度が出ています。
いろいろ実験してみた結果ですが、
- create_string_buffer はmutableではないので変更できない。そのためバイナリデータ(カメラ画像とか計測データ)の扱いには向いていない
- バイナリデータを格納する配列自体はbytearrayで作るのがよい
- バイナリな配列は (c_ubyte * 長さ).from_buffer(バイトアレイのバッファ) でキャストする
- 関数の引数にポインタを与えて戻り値を受け取るには、
uint16_t * は POINTER(c_uint16) に変換する
- 関数の引数にポインタを与えてバイナリの配列を送受信する、
uint16_t * は POINTER(c_uint16) に変換する
- bytearrayで作ったバッファをbufとすると、uint8_t*配列に変換するには
(c_ubyte * length).from_buffer(buf)
- bytearrayで作ったバッファをbufとすると、uint16_t*配列に変換するには
(c_uint16 * length).from_buffer(buf)
- bytearrayで作ったバッファをbufとすると、uint32_t*配列に変換するには
(c_uint32 * length).from_buffer(buf)
-
そもそもuint16配列 やuint32配列を
data = (c_uint16 * length).from_buffer(bytearray(length * 2))
で作ってしまってもよい。
ようです。
最近のコメント