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. 参数传递和返回

本教程详细介绍了汇编函数可以支持零到三个参数的事实,这些参数必须(如果使用)命名为r0, r1r2。当代码执行时,寄存器将被初始化为这些值。

可以通过这种方式传递的数据类型是整数和内存地址。使用当前固件,可以传递和返回所有可能的 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. 使用不受支持的指令

这些可以使用如下所示的数据语句进行编码。虽然 push()pop() 支持下面的例子说明了原则。可以在 ARM v7-M 体系结构参考手册中找到必要的机器代码。注意数据调用的第一个参数如

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

表示每个后续参数都是一个两字节的数量。

1.6. 克服 MicroPython 的整数限制

Pyboard 芯片包含一个 CRC 生成器。它的使用在 MicroPython 中存在一个问题,因为返回的值涵盖了 32 位数量的全部范围,而 MicroPython 中的小整数不能在位 30 和 31 中具有不同的值。以下代码克服了这一限制,该代码使用汇编程序将结果放入转换为数组和 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 # coerce to arbitrary precision

enable_crc()
reset_crc()
for x in range(20):
    print(hex(getcrc(0)))