1. คำแนะนำและเคล็ดลับ#

ต่อไปนี้เป็นตัวอย่างการใช้งาน inline assembler และข้อมูลเกี่ยวกับวิธีหลีกเลี่ยงข้อจำกัดต่าง ๆ ในเอกสารนี้ คำว่า "assembler function" หมายถึงฟังก์ชันที่ประกาศใน Python ด้วย decorator @micropython.asm_thumb ส่วน "subroutine" หมายถึงโค้ด assembler ที่ถูกเรียกใช้จากภายใน assembler function

1.1. การแตกสาขาและ subroutine ของโค้ด#

สิ่งสำคัญที่ต้องเข้าใจคือ label มีขอบเขตเฉพาะภายใน assembler function เดียวกัน ขณะนี้ยังไม่มีวิธีที่จะเรียก subroutine ที่กำหนดในฟังก์ชันหนึ่งจากอีกฟังก์ชันหนึ่ง

ในการเรียก subroutine จะใช้คำสั่ง bl(LABEL) ซึ่งจะโอนการควบคุมไปยังคำสั่งที่อยู่ถัดจาก directive label(LABEL) และเก็บที่อยู่ผู้ส่งคืนไว้ใน link register (lr หรือ r14) สำหรับการส่งคืน จะใช้คำสั่ง bx(lr) ซึ่งทำให้การดำเนินการดำเนินต่อไปที่คำสั่งถัดจากการเรียก subroutine กลไกนี้หมายความว่าหาก subroutine จะเรียก subroutine อื่น ต้องบันทึก link register ก่อนการเรียกและกู้คืนก่อนสิ้นสุด

ตัวอย่างที่ค่อนข้างซับซ้อนต่อไปนี้แสดงการเรียกฟังก์ชัน โปรดทราบว่าจำเป็นต้องแตกสาขาข้ามการเรียก subroutine ทั้งหมดตั้งแต่เริ่มต้น: subroutine จบการทำงานด้วย 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))

ตัวอย่างโค้ดต่อไปนี้แสดงการเรียกแบบซ้อน (recursive): ลำดับ Fibonacci แบบดั้งเดิม ที่นี่ ก่อนการเรียก recursive link register จะถูกบันทึกพร้อมกับรีจิสเตอร์อื่น ๆ ที่ตรรกะของโปรแกรมต้องการให้คงอยู่

@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. การส่งผ่านอาร์กิวเมนต์และการส่งคืน#

Assembler function รองรับอาร์กิวเมนต์ได้ตั้งแต่ศูนย์ถึงสามตัว ซึ่งต้องตั้งชื่อ (หากใช้) ว่า r0, r1 และ r2 เมื่อโค้ดทำงาน รีจิสเตอร์จะถูกเริ่มต้นด้วยค่าเหล่านั้น

ชนิดข้อมูลที่สามารถส่งผ่านด้วยวิธีนี้คือจำนวนเต็มและที่อยู่ในหน่วยความจำ ด้วยเฟิร์มแวร์ปัจจุบัน ค่า 32 บิตที่เป็นไปได้ทั้งหมดสามารถส่งผ่านและส่งคืนได้ หากค่าที่ส่งคืนอาจมีบิตที่มีนัยสำคัญที่สุดถูกตั้ง ควรใช้ type hint ของ 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 array ของจำนวนเต็มเป็นอาร์กิวเมนต์ให้กับ assembler function ฟังก์ชันจะได้รับที่อยู่ของชุดจำนวนเต็มที่อยู่ติดกัน ดังนั้นอาร์กิวเมนต์หลายตัวจึงสามารถส่งผ่านเป็นองค์ประกอบของ array เดียวได้ ในทำนองเดียวกัน ฟังก์ชันสามารถส่งคืนค่าหลายค่าโดยกำหนดให้กับองค์ประกอบของ array Assembler function ไม่มีวิธีกำหนดความยาวของ array: จำเป็นต้องส่งค่านี้ให้กับฟังก์ชัน

การใช้ array นี้สามารถขยายเพื่อให้ใช้ array มากกว่าสามชุดได้ ทำได้โดยใช้ indirection: โมดูล uctypes รองรับ addressof() ซึ่งจะคืนที่อยู่ของ array ที่ส่งเป็นอาร์กิวเมนต์ ดังนั้นคุณสามารถเติมค่า integer array ด้วยที่อยู่ของ array อื่น ๆ:

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. ชนิดข้อมูลที่ไม่ใช่จำนวนเต็ม#

ชนิดข้อมูลเหล่านี้อาจจัดการได้โดยใช้ array ของชนิดข้อมูลที่เหมาะสม ตัวอย่างเช่น ข้อมูล floating point ความแม่นยำเดี่ยวอาจประมวลผลได้ดังนี้ ตัวอย่างโค้ดนี้รับ array ของ float และแทนที่เนื้อหาด้วยกำลังสองของแต่ละค่า

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 รองรับการใช้โครงสร้างข้อมูลนอกเหนือจาก array ธรรมดา มันทำให้สามารถแมปโครงสร้างข้อมูล Python กับอินสแตนซ์ bytearray ซึ่งสามารถส่งไปยัง assembler function ได้

1.3. ค่าคงที่ที่มีชื่อ#

โค้ด assembler สามารถทำให้อ่านง่ายและบำรุงรักษาได้ง่ายขึ้นโดยใช้ค่าคงที่ที่มีชื่อแทนการใช้ตัวเลขกระจายอยู่ในโค้ด ซึ่งสามารถทำได้ดังนี้:

MYDATA = const(33)

@micropython.asm_thumb
def foo():
    mov(r0, MYDATA)

โครงสร้าง const() ทำให้ MicroPython แทนที่ชื่อตัวแปรด้วยค่าของมันในเวลา compile หากค่าคงที่ถูกประกาศในขอบเขต Python ภายนอก สามารถแชร์ระหว่าง assembler function หลายฟังก์ชันและกับโค้ด Python ได้

1.4. โค้ด Assembler ในรูปแบบ class method#

MicroPython ส่งที่อยู่ของอินสแตนซ์ object เป็นอาร์กิวเมนต์แรกให้กับ class method ปกติแล้วสิ่งนี้มีประโยชน์น้อยมากสำหรับ assembler function สามารถหลีกเลี่ยงได้โดยการประกาศฟังก์ชันเป็น static method ดังนี้:

class foo:
  @staticmethod
  @micropython.asm_thumb
  def bar(r0):
    add(r0, r0, r0)

1.5. การใช้คำสั่งที่ไม่รองรับ#

คำสั่งเหล่านี้สามารถเขียนโค้ดได้โดยใช้ data statement ดังที่แสดงด้านล่าง แม้ว่า push() และ pop() จะรองรับ แต่ตัวอย่างด้านล่างแสดงหลักการดังกล่าว machine code ที่จำเป็นอาจพบได้ใน ARM v7-M Architecture Reference Manual โปรดทราบว่าอาร์กิวเมนต์แรกของการเรียก data เช่น

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

ระบุว่าอาร์กิวเมนต์ถัดไปแต่ละตัวเป็นปริมาณสองไบต์

1.6. การเอาชนะข้อจำกัดจำนวนเต็มของ MicroPython#

จำนวนเต็มขนาดเล็กของ MicroPython บนพอร์ต 32 บิตไม่สามารถเก็บค่าที่บิต 30 และ 31 แตกต่างกันได้ (เช่น 0x80000000) ดังนั้น assembler routine ที่ให้ผลลัพธ์ 32 บิตเต็มจึงไม่สามารถส่งคืนโดยตรงได้ ข้อจำกัดนี้สามารถเอาชนะได้ด้วยโค้ดต่อไปนี้ ซึ่งใช้ assembler เพื่อใส่ผลลัพธ์ลงใน array และโค้ด 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