1. 提示与技巧¶
下面是一些使用内联汇编器的示例,以及关于如何规避其局限性的一些说明。在本文档中,术语"汇编函数"指的是在 Python 中使用 @micropython.asm_thumb 装饰器声明的函数,而"子程序"指的是从汇编函数内部调用的汇编代码。
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. 参数传递与返回¶
汇编函数可以支持零到三个参数,这些参数(如果使用的话)必须命名为 r0、r1 和 r2。当代码执行时,这些寄存器将被初始化为相应的值。
可以通过这种方式传递的数据类型是整数和内存地址。在当前固件下,所有可能的 32 位值都可以传递和返回。如果返回值的最高有效位可能被置位,则应使用 Python 类型提示,以便 MicroPython 确定该值应解释为有符号整数还是无符号整数:类型为 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 整数数组作为参数传递给汇编函数,该函数将接收到一组连续整数的地址。因此可以将多个参数作为单个数组的元素传递。类似地,函数可以通过将多个值赋给数组元素来返回多个值。汇编函数无法确定数组的长度:这需要传递给函数。
对数组的这种用法可以扩展到使用三个以上的数组。这是通过间接寻址实现的: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