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