1. ヒントとコツ

以下は、インラインアセンブラの使用例と、その制限を回避する方法についての情報です。本ドキュメントでは、「アセンブラ関数」という用語は @micropython.asm_thumb デコレータを付けて Python で宣言された関数を指し、「サブルーチン」はアセンブラ関数内から呼び出されるアセンブラコードを指します。

1.1. コードの分岐とサブルーチン

ラベルがアセンブラ関数に対してローカルであることを理解しておくことが重要です。現時点では、ある関数で定義されたサブルーチンを別の関数から呼び出す方法はありません。

サブルーチンを呼び出すには、命令 bl(LABEL) を発行します。これは label(LABEL) ディレクティブに続く命令へ制御を移し、戻りアドレスをリンクレジスタ(lr または r14)に格納します。戻るには命令 bx(lr) を発行し、これによりサブルーチン呼び出しに続く命令から実行が継続されます。この仕組みは、あるサブルーチンが別のサブルーチンを呼び出す場合、その呼び出しの前にリンクレジスタを保存し、終了する前に復元しなければならないことを意味します。

以下のやや作為的な例は関数呼び出しを示しています。最初にすべてのサブルーチン呼び出しを飛び越えて分岐する必要がある点に注意してください。サブルーチンは bx(lr) で実行を終えますが、外側の関数は Python 関数のように単に「末尾に到達して」終了します。

@micropython.asm_thumb
def quad(r0):
    b(START)
    label(DOUBLE)
    add(r0, r0, r0)
    bx(lr)
    label(START)
    bl(DOUBLE)
    bl(DOUBLE)

print(quad(10))

以下のコード例は、ネストした(再帰的な)呼び出しを示しています。古典的なフィボナッチ数列です。ここでは、再帰呼び出しの前に、リンクレジスタが、プログラムロジック上保持しておく必要のある他のレジスタとともに保存されます。

@micropython.asm_thumb
def fib(r0):
    b(START)
    label(DOFIB)
    push({r1, r2, lr})
    cmp(r0, 1)
    ble(FIBDONE)
    sub(r0, 1)
    mov(r2, r0) # r2 = n -1
    bl(DOFIB)
    mov(r1, r0) # r1 = fib(n -1)
    sub(r0, r2, 1)
    bl(DOFIB)   # r0 = fib(n -2)
    add(r0, r0, r1)
    label(FIBDONE)
    pop({r1, r2, lr})
    bx(lr)
    label(START)
    bl(DOFIB)

for n in range(10):
    print(fib(n))

1.2. 引数の受け渡しと戻り値

アセンブラ関数は 0 個から 3 個の引数をサポートでき、(使用する場合は)r0r1r2 という名前でなければなりません。コードが実行されると、これらのレジスタがその値に初期化されます。

この方法で渡せるデータ型は整数とメモリアドレスです。現行のファームウェアでは、可能なすべての 32 ビット値を渡したり返したりできます。戻り値の最上位ビットがセットされている可能性がある場合、その値を符号付き整数と符号なし整数のどちらとして解釈すべきかを MicroPython が判断できるように、Python の型ヒントを使用すべきです。型は int または uint です。

@micropython.asm_thumb
def uadd(r0, r1) -> uint:
    add(r0, r0, r1)

hex(uadd(0x40000000,0x40000000)) は 0x80000000 を返し、ビット 30 とビット 31 が異なる整数の受け渡しと戻りを示します。

引数と戻り値の数の制限は、任意の型の任意の数の値にアクセスできるようにする array モジュールを使うことで克服できます。

1.2.1. 複数の引数

Python の整数配列がアセンブラ関数に引数として渡されると、関数は連続した整数群のアドレスを受け取ります。したがって、複数の引数を単一の配列の要素として渡すことができます。同様に、関数は値を配列要素に代入することで複数の値を返すことができます。アセンブラ関数には配列の長さを判定する手段がないため、これは関数に渡す必要があります。

この配列の使い方を拡張すると、3 つを超える配列を使用できます。これは間接参照を用いて行います。uctypes モジュールは、引数として渡された配列のアドレスを返す addressof() をサポートしています。したがって、整数配列を他の配列のアドレスで埋めることができます。

from uctypes import addressof
@micropython.asm_thumb
def getindirect(r0):
    ldr(r0, [r0, 0]) # Address of array loaded from passed array
    ldr(r0, [r0, 4]) # Return element 1 of indirect array (24)

def testindirect():
    a = array.array('i',[23, 24])
    b = array.array('i',[0,0])
    b[0] = addressof(a)
    print(getindirect(b))

1.2.2. 整数以外のデータ型

これらは適切なデータ型の配列を用いて扱うことができます。たとえば、単精度浮動小数点データは次のように処理できます。このコード例は、float の配列を受け取り、その内容をそれぞれの二乗で置き換えます。

from array import array

@micropython.asm_thumb
def square(r0, r1):
    label(LOOP)
    vldr(s0, [r0, 0])
    vmul(s0, s0, s0)
    vstr(s0, [r0, 0])
    add(r0, 4)
    sub(r1, 1)
    bgt(LOOP)

a = array('f', (x for x in range(10)))
square(a, len(a))
print(a)

uctypes モジュールは、単純な配列を超えるデータ構造の使用をサポートしています。Python のデータ構造を bytearray インスタンスにマッピングし、それをアセンブラ関数に渡すことができます。

1.3. 名前付き定数

数値をコードに散りばめる代わりに名前付き定数を使用することで、アセンブラコードをより読みやすく、保守しやすくできます。これは次のようにして実現できます。

MYDATA = const(33)

@micropython.asm_thumb
def foo():
    mov(r0, MYDATA)

const() 構文は、コンパイル時に MicroPython が変数名をその値で置き換えるようにします。定数を外側の Python スコープで宣言すると、複数のアセンブラ関数間で、また Python コードと共有できます。

1.4. クラスメソッドとしてのアセンブラコード

MicroPython は、オブジェクトインスタンスのアドレスをクラスメソッドの最初の引数として渡します。これは通常、アセンブラ関数にとってほとんど役に立ちません。次のように関数を静的メソッドとして宣言することで回避できます。

class foo:
  @staticmethod
  @micropython.asm_thumb
  def bar(r0):
    add(r0, r0, r0)

1.5. サポートされていない命令の使用

これらは以下に示すように data 文を使ってコーディングできます。push()pop() はサポートされていますが、以下の例は原理を示すためのものです。必要なマシンコードは ARM v7-M Architecture Reference Manual に記載されています。次のような data 呼び出しの最初の引数

data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11

は、後続の各引数が 2 バイトの量であることを示します。

1.6. MicroPython の整数制限の克服

32 ビットポートの MicroPython の小整数は、ビット 30 とビット 31 が異なる値(たとえば 0x80000000)を保持できません。そのため、完全な 32 ビットの結果を生成するアセンブラルーチンは、それを単純に直接返すことができません。この制限は、アセンブラを使って結果を配列に格納し、Python コードを使って結果を任意精度の符号なし整数に強制変換する、以下のコードによって克服されます。

from array import array

@micropython.asm_thumb
def getval(r0):
    movwt(r1, 0x80000000)  # a 32-bit value whose bits 30 and 31 differ
    str(r1, [r0, 0])

def get():
    a = array('i', [0])
    getval(a)
    return a[0] & 0xffffffff  # coerce to arbitrary precision

print(hex(get()))  # 0x80000000