uctypes -- 構造化手法でのバイナリデータアクセス

このモジュールは MicroPython 用の「外部データインターフェース」を実装しています。その背後にある考え方は CPython の ctypes モジュールと似ていますが、実際の API は異なり、サイズを小さくするために合理化、最適化されています。このモジュールの基本的な考え方は、C言語でできることとほぼ同じくデータ構造のレイアウトを定義し、よく知られたドット構文を使ってサブフィールドにアクセスできるようにすることです。

警告

uctypes モジュールはマシンの任意のメモリアドレス(I/O と制御レジスタを含む)へのアクセスを許可します。それを不当に使用すると、クラッシュ、データの損失、さらにはハードウェアの誤動作につながる可能性があります。

参考

モジュール struct
バイナリデータ構造にアクセスするための標準的な Python の手法(大規模で複雑な構造には適していません)。

使用例:

import uctypes

# 例1: ELF ファイルヘッダのサブセット
# 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" はバイナリモードでオープンした ELF ファイル
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))


# 例2: ポインターのあるインメモリデータ構造
COORD = {
    "x": 0 | uctypes.FLOAT32,
    "y": 4 | uctypes.FLOAT32,
}

STRUCT1 = {
    "data1": 0 | uctypes.UINT8,
    "data2": 4 | uctypes.UINT32,
    "ptr": (8 | uctypes.PTR, COORD),
}

# "addr" に型 STRUCT1 の構造体のアドレスがあるとします
# uctypes.NATIVE はオプション(デフォルトで使用)
struct1 = uctypes.struct(addr, STRUCT1, uctypes.NATIVE)
print("x:", struct1.ptr[0].x)


# 例3: CPU レジスタのアクセス。STM32F4xx WWDG ブロックのサブセット
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 は、各フィールドに対してオフセットの明示的な指定が必要です。オフセットは構造体の先頭からのバイト数で与えられます。

以下は、さまざまなフィールドタイプのエンコード例です:

  • スカラー型:

    "フィールド名": オフセット | uctypes.UINT32
    

    つまり、値は、構造体の先頭からのフィールドオフセット(バイト単位)とスカラ型識別子のビット論理和です。

  • 再帰構造:

    "sub": (offset, {
        "b0": 0 | uctypes.UINT8,
        "b1": 1 | uctypes.UINT8,
    })
    

    つまり、value は2項目のタプルで、1番目の項目はオフセット、2番目の項目は構造体デスクリプター辞書です(注: 再帰記述子のオフセットは、定義している構造体に対する相対位置です)。もちろん、再帰構造はリテラル辞書だけではなく、(以前に定義した)構造体デスクリプター辞書を名前で参照することによっても指定できます。

  • プリミティブ型の配列:

    "arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),
    

    つまり、value は2項目のタプルで、1番目の項目は ARRAY フラグとオフセットのビット論理和で、2番目の項目はスカラー型と配列要素数のビット論理和です。

  • 集約タイプの配列:

    "arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),
    

    つまり、valueは3項目のタプルで、1番目の項目は ARRAY フラグと offset のビット論理和、2番目の項目は配列内の要素数、3番目の項目は要素型のデスクリプターです。

  • プリミティブ型へのポインタ:

    "ptr": (offset | uctypes.PTR, uctypes.UINT8),
    

    すなわち、valueは2項目のタプルで、1番目の項目はオフセットとPTRフラグのビット論理和、2番目の項目はスカラー要素型です。

  • 集約型へのポインタ:

    "ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),
    

    すなわち、valueは2項目のタプルで、1番目の項目はオフセットとOTRフラグのビット論理和、2番目の項目はポインタが指す型のデスクリプターです。

  • ビットフィールド:

    "bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,
    

    すなわち、value は3つのビット論理和で、1つ目は与えたビットフィールドを含むスカラ値の型(typenameはスカラ型に似ていますが、 BF 接頭辞が付きます)、2つ目はビットフィールドを含むスカラ値のためのオフセット、3つ目はビット位置とビット長をそれぞれ BF_POS と BF_LEN でシフトされたスカラー値です。ビットフィールド位置は、スカラーの最下位ビット(位置0)からカウントされ、フィールドの右端のビット数です(つまり、スカラーからビットフィールドを抽出するために右にシフトするのに必要なビット数です)。

    上記の例では、最初に UINT16 値がオフセット 0 で抽出され(この詳細は、特定のアクセスサイズとアライメントが必要なハードウェアレジスタにアクセスするときに重要です)、次にこの UINT16 の最右ビットが lsbit ビットであるビットフィールド長さはビットサイズのビットとして抽出されます。たとえば、 lsbit が 0 で bitsize が 8 の場合、事実上 UINT16 の最下位バイトにアクセスします。

    ビットフィールド演算は、ターゲットバイトのエンディアンとは無関係です。特に上の例では、リトルエンディアン構造とビッグエンディアン構造の両方で、UINT16 の最下位バイトにアクセスします。しかし、それは 0 と番号付けされている最下位ビットに依存します。いくつかのターゲットはそれらのネイティブ ABI で異なる番号付けを使用するかもしれませんが、 uctypes は常に上記の正規化された番号付けを使用します。

モジュールの内容

class uctypes.struct(addr, descriptor, layout_type=NATIVE, /)

メモリ内の構造体アドレス、デスクリプター(辞書としてエンコードされている)、レイアウトタイプ(下記参照)に基づいて「外部データ構造」オブジェクトをインスタンス化します。

uctypes.LITTLE_ENDIAN

リトルエンディアンのパック構造のレイアウトタイプ。(パックとは、すべてのフィールドがデスクリプターで定義されているのとまったく同じバイト数を占めることを意味します。つまり、アライメントは1です)。

uctypes.BIG_ENDIAN

ビッグエンディアンのパック構造のレイアウトタイプ。

uctypes.NATIVE

ネイティブ構造体のレイアウトタイプ - MicroPython が実行されているシステムの ABI に準拠したデータのエンディアンとアライメントを持ちます。

uctypes.sizeof(struct, layout_type=NATIVE, /)

データ構造のサイズをバイト数で返します。struct 引数は、構造体クラスまたは特定のインスタンス化された構造物(またはその集合体フィールド)のいずれかです。

uctypes.addressof(obj)

オブジェクトのアドレスを返します。引数は、bytes, bytearray, バッファプロトコルをサポートする他のオブジェクトである必要があります(このバッファのアドレスは実際に返されるものです)。

uctypes.bytes_at(addr, size)

指定されたアドレスとサイズのメモリをバイトオブジェクトとしてキャプチャします。bytes 型オブジェクトは不変なので、メモリは実際には複製されて bytes 型オブジェクトにコピーされます。したがって、メモリの内容が後で変更されても、作成されたオブジェクトは元の値を保持します。

uctypes.bytearray_at(addr, size)

与えたアドレスとサイズのメモリを bytearray オブジェクトとしてキャプチャします。上記の bytes_at() 関数とは異なり、メモリは参照によって取得されるため、両方とも書き込むことができ、指定されたメモリアドレスで現在の値にアクセスします。

uctypes.UINT8
uctypes.INT8
uctypes.UINT16
uctypes.INT16
uctypes.UINT32
uctypes.INT32
uctypes.UINT64
uctypes.INT64

構造体デスクリプターの整数型 符号付きと符号なしの両方の 8, 16, 32, 64 ビット型の定数が提供されています。

uctypes.FLOAT32
uctypes.FLOAT64

構造体デスクリプターの浮動小数点型

uctypes.VOID

VOID これは UINT8 の別名であり、C の void ポインターのために定義されています: (uctypes.PTR, uctypes.VOID)

uctypes.PTR
uctypes.ARRAY

ポインタと配列の型定数。構造体のための明示的な定数がないことに注意してください、それは暗黙のうちに:PTR またはARRAYフラグのない集約型は構造体です。

構造体デスクリプターと構造化オブジェクトのインスタンス化

構造体デスクリプター辞書とそのレイアウトタイプが与えられた場合、 uctypes.struct() コンストラクタを使用して特定のメモリインスタンスに特定の構造体インスタンスをインスタンス化できます。メモリアドレスは通常、以下のソースから取得されます。

  • ベアメタルシステムのハードウェアレジスタにアクセスするときの定義済みアドレス。特定の MCU/SoC のデータシートでこれらのアドレスを検索してください。
  • FFI (外部関数インタフェース)関数への呼び出しからの戻り値として。
  • uctypes.addressof() から、FFI 関数に引数を渡したいとき、または I/O 用のデータ(ファイルやネットワークソケットから読み込んだデータなど)にアクセスしたいとき。

構造体オブジェクト

構造体オブジェクトは標準のドット表記法 my_struct.substruct1.field1 を使って個々のフィールドにアクセスすることを可能にします。フィールドがスカラー型の場合、それを取得すると、フィールドに含まれる値に対応するプリミティブ値(Python の整数または浮動小数点数)が生成されます。スカラーフィールドも割り当てることができます。

フィールドが配列の場合、その個々の要素は標準の添字演算子 [] を使って読み取りと代入の両方でアクセスできます。

フィールドがポインターの場合は、[0] 構文を使ってポインターの指す先を参照できます(C の * 演算子に対応しますが、C で [0] とすることと同様です)。C と同じセマンティクスで 0 以外の整数値でポインタを添字にすることもサポートされています。

まとめると、構造体フィールドへのアクセスは一般に C の構文にしたがいますが、ポインタの指す先の参照には * の代わりに [0] を使う必要があります。

制限事項

  1. 非スカラーフィールドにアクセスすると、それらを表す中間オブジェクトが割り当てられます。これは、(たとえば割り込みハンドラの処理など)メモリ割り付けが無効になっているときにアクセスする必要がある構造をレイアウトするのに特別な注意を払うべきであることを意味します。推奨事項は次のとおりです:
  • 入れ子構造にアクセスしないでください。たとえば、 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] のようにしてキャッシュすることです。
  1. uctypes モジュールでサポートされているオフセットの範囲は限られています。サポートされている正確な範囲は実装の詳細と見なされます。一般的な提案は、構造定義を分割して最大で数キロバイトから数十キロバイトまでになるようにすることです。ほとんどの場合、これはとにかく自然な状況です。たとえば、(32ビットアドレス空間に広がる) MCU のすべてのレジスタを1つの構造体で定義するのは意味がなく、むしろペリフェラルブロックごとに定義するべきです。極端な場合には、構造をいくつかの部分に人為的に分割する必要があるかもしれません(たとえば、数メガバイトの配列を中央に持つネイティブデータ構造にアクセスする場合などは、人為的な分割せざろうえないケースです)。