1. 提示與技巧

以下是內聯組合語言(inline assembler)使用方式的一些範例,以及如何繞過其限制的相關資訊。在本文件中,「組語函式(assembler function)」一詞是指以 @micropython.asm_thumb 裝飾器在 Python 中宣告的函式,而「子常式(subroutine)」則是指從組語函式內部呼叫的組語程式碼。

1.1. 程式碼分支與子常式

務必理解標籤(label)是組語函式的區域性元素。目前沒有任何方式可以讓在某個函式中定義的子常式從另一個函式被呼叫。

要呼叫子常式需發出 bl(LABEL) 指令。此指令會將控制權轉移到 label(LABEL) 指示詞後面的指令,並將返回位址儲存在連結暫存器(lrr14)中。要返回時則發出 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))

以下程式碼範例示範了一次巢狀(遞迴)呼叫:經典的費氏數列(Fibonacci sequence)。此處在進行遞迴呼叫之前,連結暫存器會與程式邏輯要求保留的其他暫存器一起被儲存。

@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. 引數傳遞與返回

組語函式可支援零到三個引數,若使用則必須命名為 r0r1r2。當程式碼執行時,這些暫存器會被初始化為這些值。

能以此方式傳遞的資料型別為整數與記憶體位址。在目前的韌體下,所有可能的 32 位元值皆可傳遞與返回。若返回值的最高有效位元可能被設定,則應採用 Python 型別提示,以便讓 MicroPython 判斷該值應被解讀為有號或無號整數:型別為 intuint

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

hex(uadd(0x40000000,0x40000000)) 會返回 0x80000000,示範了第 30 與 31 位元相異的整數之傳遞與返回。

引數數量與返回值數量上的限制,可透過 array 模組來克服,該模組能存取任意數量、任意型別的值。

1.2.1. 多個引數

若將一個 Python 整數陣列作為引數傳遞給組語函式,該函式將接收到一組連續整數的位址。因此,多個引數可作為單一陣列的元素來傳遞。同樣地,函式也可藉由將多個值指派給陣列元素來返回多個值。組語函式無法判斷陣列的長度:這需要另外傳遞給函式。

陣列的這種用法可以延伸,以便使用超過三個陣列。這是透過間接定址(indirection)達成的: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. 非整數資料型別

這些可藉由使用適當資料型別的陣列來處理。例如,單精度浮點資料可按如下方式處理。此程式碼範例接收一個浮點數陣列,並將其內容替換為各元素的平方。

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

表示後續每個引數皆為兩個位元組的量。

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