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 個の引数をサポートします。引数を使う場合は r0 、 r1 、 r2 という名前でなければなりません。コードの実行時には、これらのレジスタがその値で初期化されます。
この方法で渡せるデータ型は、整数とメモリアドレスです。現在のファームウェアでは、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 の整数の配列をアセンブラ関数の引数として渡すと、関数は連続した整数の集まりのアドレスを受け取ります。したがって、1つの配列の要素として複数の引数を渡せます。同様に、配列の要素に代入することで関数から複数の値を返せます。アセンブラ関数には配列の長さを知る手段がないので、これは関数に渡す必要があります。
この配列の使い方を拡張すると、3つを超える配列を使えます。これは間接参照によって行います。 uctypes モジュールは addressof() をサポートしており、引数として渡された配列のアドレスを返します。したがって、整数の配列に他の配列のアドレスを詰めることができます:
from uctypes import addressof
@micropython.asm_thumb
def getindirect(r0):
ldr(r0, [r0, 0]) # 渡された配列から配列のアドレスをロード
ldr(r0, [r0, 4]) # 間接配列の要素1 (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 の配列を受け取り、その内容をそれぞれの2乗で置き換えます。
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 は、クラスのメソッドの第1引数としてオブジェクトのインスタンスのアドレスを渡します。これは通常、アセンブラ関数にとってほとんど役に立ちません。次のように関数を静的メソッドとして宣言すれば回避できます:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. サポートされていない命令の使用
これらは、以下に示すように data 文を使って記述できます。 push() と pop() はサポートされていますが、以下の例はその原理を示すものです。必要なマシンコードは ARM v7-M アーキテクチャリファレンスマニュアルにあります。次のような data 呼び出しの最初の引数は
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
後続の各引数が2バイトの量であることを示しています。
1.6. MicroPython の整数の制限を克服する
Pyboard のチップには CRC ジェネレータが含まれています。返される値が 32 ビットの値の全域にわたるのに対し、MicroPython の小さな整数はビット 30 と 31 で異なる値を持てないため、これを使うと MicroPython では問題になります。この制限は次のコードで克服できます。ここでは、アセンブラで結果を配列に入れ、Python のコードでその結果を任意精度の符号なし整数に変換しています。
from array import array
import stm
def enable_crc():
stm.mem32[stm.RCC + stm.RCC_AHB1ENR] |= 0x1000
def reset_crc():
stm.mem32[stm.CRC+stm.CRC_CR] = 1
@micropython.asm_thumb
def getval(r0, r1):
movwt(r3, stm.CRC + stm.CRC_DR)
str(r1, [r3, 0])
ldr(r2, [r3, 0])
str(r2, [r0, 0])
def getcrc(value):
a = array('i', [0])
getval(a, value)
return a[0] & 0xffffffff # 任意精度に変換
enable_crc()
reset_crc()
for x in range(20):
print(hex(getcrc(0)))