1. 提示与技巧

下面是一些使用内联汇编器的示例,以及关于如何规避其局限性的一些说明。在本文档中,术语"汇编函数"指的是在 Python 中使用 @micropython.asm_thumb 装饰器声明的函数,而"子程序"指的是从汇编函数内部调用的汇编代码。

1.1. 代码分支与子程序

需要重点理解的是,标签是汇编函数的局部对象。目前还无法从一个函数中调用在另一个函数中定义的子程序。

要调用子程序,需发出 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))

下面的代码示例演示了一次嵌套(递归)调用:经典的斐波那契数列。这里,在进行递归调用之前,链接寄存器会与程序逻辑要求保留的其他寄存器一起被保存。

@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 整数数组作为参数传递给汇编函数,该函数将接收到一组连续整数的地址。因此可以将多个参数作为单个数组的元素传递。类似地,函数可以通过将多个值赋给数组元素来返回多个值。汇编函数无法确定数组的长度:这需要传递给函数。

对数组的这种用法可以扩展到使用三个以上的数组。这是通过间接寻址实现的: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