uctypes --- バイナリデータへの構造化されたアクセス¶
このモジュールは、MicroPython向けの「外部データインターフェース」を実装します。その背後にある考え方は CPython の ctypes モジュールと似ていますが、実際の API は異なり、小さなサイズに合わせて合理化・最適化されています。このモジュールの基本的な考え方は、C言語が許容するのとほぼ同等の表現力でデータ構造のレイアウトを定義し、馴染みのあるドット構文を使ってサブフィールドを参照しながらアクセスする、というものです。
警告
uctypes モジュールを使うと、マシンの任意のメモリアドレス(I/Oレジスタや制御レジスタを含む)にアクセスできます。不注意な使い方は、クラッシュ、データ損失、さらにはハードウェアの誤動作を招くおそれがあります。
参考
structモジュールバイナリデータのパックおよびアンパックを行う標準の Python モジュールです。
structはコンパクトなフォーマット文字列(例:'<HBB4sI')を使ってバッファ全体を一度に操作します。これは少数の固定フィールドには適していますが、大きな構造体や深くネストされた構造体には不向きです。読み書きのたびにフォーマット文字列が再解析され、共用体やビットフィールドはサポートされず、既存のバッファに対する型付きビューを得る手段もありません。uctypesはstructを補完するもので、レイアウトを一度記述してメモリ領域(RAM、ペリフェラルレジスタ、bytearray)に結び付け、個々のフィールドを名前付き属性としてアクセスできるようにします。これにより、繰り返しの解析やコピーを避けつつ、ネストされた構造体、配列、共用体、ビットフィールドのサポートも追加されます。
使用例:
import uctypes
# Example 1: Subset of ELF file header
# https://wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
ELF_HEADER = {
"EI_MAG": (0x0 | uctypes.ARRAY, 4 | uctypes.UINT8),
"EI_DATA": 0x5 | uctypes.UINT8,
"e_machine": 0x12 | uctypes.UINT16,
}
# "f" is an ELF file opened in binary mode
buf = f.read(uctypes.sizeof(ELF_HEADER, uctypes.LITTLE_ENDIAN))
header = uctypes.struct(uctypes.addressof(buf), ELF_HEADER, uctypes.LITTLE_ENDIAN)
assert header.EI_MAG == b"\x7fELF"
assert header.EI_DATA == 1, "Oops, wrong endianness. Could retry with uctypes.BIG_ENDIAN."
print("machine:", hex(header.e_machine))
# Example 2: In-memory data structure, with pointers
COORD = {
"x": 0 | uctypes.FLOAT32,
"y": 4 | uctypes.FLOAT32,
}
STRUCT1 = {
"data1": 0 | uctypes.UINT8,
"data2": 4 | uctypes.UINT32,
"ptr": (8 | uctypes.PTR, COORD),
}
# Suppose you have address of a structure of type STRUCT1 in "addr"
# uctypes.NATIVE is optional (used by default)
struct1 = uctypes.struct(addr, STRUCT1, uctypes.NATIVE)
print("x:", struct1.ptr[0].x)
# Example 3: Access to CPU registers. Subset of STM32F4xx WWDG block
WWDG_LAYOUT = {
"WWDG_CR": (0, {
# BFUINT32 here means size of the WWDG_CR register
"WDGA": 7 << uctypes.BF_POS | 1 << uctypes.BF_LEN | uctypes.BFUINT32,
"T": 0 << uctypes.BF_POS | 7 << uctypes.BF_LEN | uctypes.BFUINT32,
}),
"WWDG_CFR": (4, {
"EWI": 9 << uctypes.BF_POS | 1 << uctypes.BF_LEN | uctypes.BFUINT32,
"WDGTB": 7 << uctypes.BF_POS | 2 << uctypes.BF_LEN | uctypes.BFUINT32,
"W": 0 << uctypes.BF_POS | 7 << uctypes.BF_LEN | uctypes.BFUINT32,
}),
}
WWDG = uctypes.struct(0x40002c00, WWDG_LAYOUT)
WWDG.WWDG_CFR.WDGTB = 0b10
WWDG.WWDG_CR.WDGA = 1
print("Current counter:", WWDG.WWDG_CR.T)
構造体レイアウトの定義¶
構造体のレイアウトは「ディスクリプタ」によって定義されます。これは、フィールド名をキーとして、それらにアクセスするために必要なその他のプロパティを対応する値としてエンコードした Python の辞書です:
{
"field1": <properties>,
"field2": <properties>,
...
}
現在、uctypes は各フィールドのオフセットを明示的に指定する必要があります。オフセットは構造体の先頭からのバイト数で与えます。
以下は、さまざまなフィールド型のエンコード例です:
スカラー型:
"field_name": offset | uctypes.UINT32言い換えると、値はスカラー型識別子と、構造体の先頭からのフィールドオフセット(バイト単位)を OR したものです。
再帰構造体:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
つまり、値は2要素のタプルで、最初の要素はオフセット、2番目の要素は構造体ディスクリプタの辞書です(注意: 再帰ディスクリプタ内のオフセットは、それが定義する構造体に対する相対値です)。もちろん、再帰構造体はリテラルの辞書だけでなく、(あらかじめ定義された)構造体ディスクリプタの辞書を名前で参照することによっても指定できます。
プリミティブ型の配列:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),つまり、値は2要素のタプルで、最初の要素は ARRAY フラグとオフセットを OR したもの、2番目の要素はスカラー要素型と配列の要素数を OR したものです。
集約型の配列:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),つまり、値は3要素のタプルで、最初の要素は ARRAY フラグとオフセットを OR したもの、2番目は配列の要素数、3番目は要素型のディスクリプタです。
プリミティブ型へのポインタ:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),つまり、値は2要素のタプルで、最初の要素は PTR フラグとオフセットを OR したもの、2番目はスカラー要素型です。
集約型へのポインタ:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),つまり、値は2要素のタプルで、最初の要素は PTR フラグとオフセットを OR したもの、2番目は指し示す型のディスクリプタです。
ビットフィールド:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,つまり、値は指定されたビットフィールドを含むスカラー値の型(型名はスカラー型と似ていますが、接頭辞に
BFが付きます)に、ビットフィールドを含むスカラー値のオフセットを OR し、さらにスカラー値内におけるビットフィールドのビット位置とビット長の値を、それぞれ BF_POS ビットと BF_LEN ビットだけシフトして OR したものです。ビットフィールドの位置はスカラーの最下位ビット(位置0)から数えられ、フィールドの最も右側のビットの番号を表します(言い換えると、ビットフィールドを取り出すためにスカラーを右へシフトする必要があるビット数です)。上記の例では、まずオフセット0で UINT16 の値が取り出されます(この点は、特定のアクセスサイズやアラインメントが要求されるハードウェアレジスタにアクセスする際に重要になることがあります)。次に、この UINT16 の最も右側のビットが lsbit ビットで、長さが bitsize ビットであるビットフィールドが取り出されます。例えば lsbit が0で bitsize が8の場合、実質的に UINT16 の最下位バイトにアクセスすることになります。
ビットフィールド操作はターゲットのバイトエンディアンに依存しないことに注意してください。特に、上記の例はリトルエンディアン構造体でもビッグエンディアン構造体でも UINT16 の最下位バイトにアクセスします。ただし、これは最下位ビットが0番として番号付けされていることに依存します。一部のターゲットはネイティブの ABI で異なる番号付けを使う場合がありますが、
uctypesは常に上記で説明した正規化された番号付けを使用します。
モジュールの内容¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
メモリ内の構造体アドレス、ディスクリプタ(辞書としてエンコードされたもの)、およびレイアウト型(後述)に基づいて、「外部データ構造」オブジェクトをインスタンス化します。
- uctypes.LITTLE_ENDIAN: int¶
リトルエンディアンのパック構造体のレイアウト型です。(パックとは、各フィールドがディスクリプタで定義されたとおりのバイト数だけを占めること、すなわちアラインメントが1であることを意味します)。
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
データ構造のサイズをバイト単位で返します。struct 引数には、構造体クラスまたは特定のインスタンス化された構造体オブジェクト(あるいはその集約フィールド)のいずれかを指定できます。
- uctypes.addressof(obj: Any) int¶
オブジェクトのアドレスを返します。引数は bytes、bytearray、またはバッファプロトコルをサポートするその他のオブジェクトである必要があります(実際に返されるのは、このバッファのアドレスです)。
- uctypes.bytes_at(addr: int, size: int) bytes¶
指定されたアドレスとサイズのメモリを bytes オブジェクトとしてキャプチャします。bytes オブジェクトは不変であるため、メモリは実際には複製されて bytes オブジェクトへコピーされます。したがって、後でメモリの内容が変化しても、作成されたオブジェクトは元の値を保持します。
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
指定されたアドレスとサイズのメモリを bytearray オブジェクトとしてキャプチャします。上記の bytes_at() 関数とは異なり、メモリは参照によってキャプチャされるため、書き込みも可能であり、指定されたメモリアドレスにある現在の値にアクセスできます。
スカラー整数型です。それぞれ明らかなバイト数(1、2、4、または 8)を占め、構造体のレイアウト型のエンディアン(NATIVE、LITTLE_ENDIAN、BIG_ENDIAN のいずれか)を使って読み書きされます。
- uctypes.VOID: int¶
UINT8の別名です。C言語スタイルのvoid *フィールドを(uctypes.PTR, uctypes.VOID)のように慣用的に記述できるように用意されています。
- uctypes.PTR: int¶
ディスクリプタのフィールドを別の型へのポインタとしてマークします。ポインタフィールドは2要素のタプル
(offset | PTR, target_type_or_descriptor)として記述します。ポインタをデリファレンスすると、それが保持するアドレスへの型付きビューが得られます。
- uctypes.ARRAY: int¶
ディスクリプタのフィールドを別の型の固定長配列としてマークします。配列フィールドは、スカラーの配列の場合は
(offset | ARRAY, count | element_type)、構造体の配列の場合は(offset | ARRAY, count, element_descriptor)のいずれかです。要素数はディスクリプタ定義時に固定されます。
構造体のための明示的な定数はありません。PTR も ARRAY も使用しない集約ディスクリプタは、構造体として扱われます。
構造体ディスクリプタと構造体オブジェクトのインスタンス化¶
構造体ディスクリプタの辞書とそのレイアウト型があれば、uctypes.struct() コンストラクタを使って、指定したメモリアドレスに特定の構造体インスタンスをインスタンス化できます。メモリアドレスは通常、次のような場所から得られます:
ベアメタルシステムでハードウェアレジスタにアクセスする場合の、あらかじめ定義されたアドレス。これらのアドレスは、特定の MCU/SoC のデータシートで調べてください。
なんらかの FFI(Foreign Function Interface)関数の呼び出しからの戻り値として。
FFI 関数に引数を渡したいとき、あるいは I/O 用のなんらかのデータ(例えばファイルやネットワークソケットから読み取ったデータ)にアクセスしたいときに
uctypes.addressof()から取得します。
構造体オブジェクト¶
構造体オブジェクトでは、標準的なドット記法を使って個々のフィールドにアクセスできます: my_struct.substruct1.field1。フィールドがスカラー型の場合、それを取得するとフィールドに含まれる値に対応するプリミティブ値(Python の整数または浮動小数点数)が得られます。スカラーフィールドへの代入も可能です。
フィールドが配列の場合、その個々の要素には標準の添字演算子 [] を使ってアクセスでき、読み取りも代入も可能です。
フィールドがポインタの場合、[0] 構文を使ってデリファレンスできます(これは C の * 演算子に対応しますが、[0] は C でも使えます)。0以外の整数値でポインタを添字付けすることもサポートされており、その意味は C と同じです。
まとめると、構造体フィールドへのアクセスは、ポインタのデリファレンスを除いて一般に C の構文に従います。デリファレンスの際には * の代わりに [0] 演算子を使う必要があります。
制限事項¶
1. Accessing non-scalar fields leads to allocation of intermediate objects to represent them. This means that special care should be taken to layout a structure which needs to be accessed when memory allocation is disabled (e.g. from an interrupt). The recommendations are:
ネストされた構造体へのアクセスを避けてください。例えば、
mcu_registers.peripheral_a.register1の代わりに、各ペリフェラルごとに別々のレイアウトディスクリプタを定義し、peripheral_a.register1としてアクセスします。あるいは、特定のペリフェラルをキャッシュするだけでもかまいません:peripheral_a = mcu_registers.peripheral_a。レジスタが複数のビットフィールドで構成されている場合は、特定のレジスタへの参照をキャッシュする必要があります:reg_a = mcu_registers.peripheral_a.reg_a。配列などのその他の非スカラーデータも避けてください。例えば、
peripheral_a.register[0]の代わりにperipheral_a.register0を使います。ここでも、中間値をキャッシュするという代替手段があります。例:register0 = peripheral_a.register[0]。
2. Range of offsets supported by the uctypes module is limited.
The exact range supported is considered an implementation detail,
and the general suggestion is to split structure definitions to
cover from a few kilobytes to a few dozen of kilobytes maximum.
In most cases, this is a natural situation anyway, e.g. it doesn't make
sense to define all registers of an MCU (spread over 32-bit address
space) in one structure, but rather a peripheral block by peripheral
block. In some extreme cases, you may need to split a structure in
several parts artificially (e.g. if accessing native data structure
with multi-megabyte array in the middle, though that would be a very
synthetic case).