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

本教程详细介绍了汇编程序函数可以支持0到3个参数这一特性,这三个参数须(若使用)命名为 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数据结构能够映射到字节数组实例,然后可将其传输给汇编程序函数。

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

表示每个后续参数为2字节值。

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)))